From 676c38643c5424df45c803d9bd487c46347fc6e7 Mon Sep 17 00:00:00 2001 From: nexxo Date: Mon, 27 Jul 2026 16:17:06 +0200 Subject: [PATCH] Follow the approved template, and stop showing invented records The dashboard was built to my own idea of the layout rather than to the template that was signed off: a coloured pill where the template has a stamped mono line, bare figures where it has a label with a chevron, a unit on its own line instead of beside the number, and no ring at all. There is no point agreeing a design and then building something adjacent to it. The metric card is now a component that draws the template's form, and the pages use it. Two more places were still showing data that does not exist: - Support listed three tickets to every customer, complete with reference numbers and dates. There is no ticket model; it was fixture text. A request list also belongs in the console, where an operator can see who filed what. Removed until there is something real behind it. - The datacentre name reached the customer through the cloud tab's copy as well. Customer surfaces name the jurisdiction; the building is operator information. And the actions column rendered an empty cell for the owner, who can be neither re-invited nor revoked. An empty cell reads as a missing feature; it now says so with a dash and a title. Co-Authored-By: Claude Opus 5 --- app/Livewire/Support.php | 9 -- lang/de/dashboard.php | 2 + lang/de/users.php | 1 + lang/en/dashboard.php | 2 + lang/en/users.php | 1 + resources/css/app.css | 40 +++++++++ .../views/components/ui/metric.blade.php | 48 +++++++++++ resources/views/components/ui/ring.blade.php | 14 ++++ resources/views/components/ui/spark.blade.php | 31 +++++++ resources/views/livewire/dashboard.blade.php | 82 ++++++++++--------- resources/views/livewire/support.blade.php | 22 ----- resources/views/livewire/users.blade.php | 3 + 12 files changed, 186 insertions(+), 69 deletions(-) create mode 100644 resources/views/components/ui/metric.blade.php create mode 100644 resources/views/components/ui/ring.blade.php create mode 100644 resources/views/components/ui/spark.blade.php diff --git a/app/Livewire/Support.php b/app/Livewire/Support.php index 1652e16..ebccf30 100644 --- a/app/Livewire/Support.php +++ b/app/Livewire/Support.php @@ -2,7 +2,6 @@ namespace App\Livewire; -use Illuminate\Support\Carbon; use Livewire\Attributes\Layout; use Livewire\Component; @@ -11,15 +10,7 @@ class Support extends Component { public function render() { - $locale = app()->getLocale(); - $date = fn (string $iso) => Carbon::parse($iso)->locale($locale)->isoFormat('LL'); - return view('livewire.support', [ - 'tickets' => [ - ['subject' => __('support.ticket_migration'), 'ref' => '#4821', 'date' => $date('2026-07-22'), 'status' => 'open'], - ['subject' => __('support.ticket_training'), 'ref' => '#4790', 'date' => $date('2026-07-18'), 'status' => 'progress'], - ['subject' => __('support.ticket_smtp'), 'ref' => '#4712', 'date' => $date('2026-07-04'), 'status' => 'closed'], - ], 'faqs' => [ ['q' => __('support.faq_q1'), 'a' => __('support.faq_a1')], ['q' => __('support.faq_q2'), 'a' => __('support.faq_a2')], diff --git a/lang/de/dashboard.php b/lang/de/dashboard.php index 3c32b4c..0d6ba0e 100644 --- a/lang/de/dashboard.php +++ b/lang/de/dashboard.php @@ -36,7 +36,9 @@ return [ 'address' => 'Adresse', 'storage' => 'Speicher', 'storage_note' => 'vertraglich zugesichert', + 'location_note' => 'kein Drittlandtransfer', 'gb' => ':n GB', + 'of_seats' => 'von :total Plätzen', 'of_total' => ':used von :total', 'per_month' => ':amount netto / Monat', 'per_year' => ':amount netto / Jahr', diff --git a/lang/de/users.php b/lang/de/users.php index 04bbec8..eab6d7b 100644 --- a/lang/de/users.php +++ b/lang/de/users.php @@ -18,6 +18,7 @@ return [ 'col_person' => 'Person', 'col_status' => 'Status', + 'owner_no_actions' => 'Der Inhaber kann nicht entfernt werden.', 'col_actions' => 'Aktionen', 'status_active' => 'Aktiv', diff --git a/lang/en/dashboard.php b/lang/en/dashboard.php index ee14e98..7cb8544 100644 --- a/lang/en/dashboard.php +++ b/lang/en/dashboard.php @@ -36,7 +36,9 @@ return [ 'address' => 'Address', 'storage' => 'Storage', 'storage_note' => 'contractually guaranteed', + 'location_note' => 'no third-country transfer', 'gb' => ':n GB', + 'of_seats' => 'of :total seats', 'of_total' => ':used of :total', 'per_month' => ':amount net / month', 'per_year' => ':amount net / year', diff --git a/lang/en/users.php b/lang/en/users.php index 705b31a..a53943b 100644 --- a/lang/en/users.php +++ b/lang/en/users.php @@ -18,6 +18,7 @@ return [ 'col_person' => 'Person', 'col_status' => 'Status', + 'owner_no_actions' => 'The owner cannot be removed.', 'col_actions' => 'Actions', 'status_active' => 'Active', diff --git a/resources/css/app.css b/resources/css/app.css index 9c71049..8109c48 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -81,3 +81,43 @@ transition-duration: 0.001ms !important; } } + +/* ── Metric visuals ─────────────────────────────────────────────────────── + * The ring and the sparkline from the approved template. They live here + * rather than as utilities because both need keyframes and a per-element + * custom property, and Tailwind expresses neither. + * + * Every var() carries a fallback: an undefined custom property does not make + * a browser skip the declaration, it computes to `unset` — which for a + * stroke-dashoffset silently draws a full ring at every value. + */ +.ring { transform: rotate(-90deg); } +.ring .track { fill: none; stroke: var(--surface-hover); stroke-width: 7; } +.ring .value { + fill: none; + stroke: var(--accent); + stroke-width: 7; + stroke-linecap: round; + stroke-dasharray: var(--c, 157); + stroke-dashoffset: var(--c, 157); + animation: ring-sweep 1.1s cubic-bezier(.2, .7, .2, 1) .2s forwards; +} +@keyframes ring-sweep { to { stroke-dashoffset: var(--o, 0); } } + +.spark path { fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; } +.spark .line { + stroke: var(--accent); + stroke-dasharray: 400; + stroke-dashoffset: 400; + animation: spark-draw 1.3s cubic-bezier(.4, .1, .2, 1) .2s forwards; +} +.spark.muted .line { stroke: var(--text-muted); } +.spark .area { fill: var(--accent); stroke: none; opacity: 0; animation: spark-fade .8s ease .9s forwards; } +@keyframes spark-draw { to { stroke-dashoffset: 0; } } +@keyframes spark-fade { to { opacity: .09; } } + +@media (prefers-reduced-motion: reduce) { + .ring .value { stroke-dashoffset: var(--o, 0) !important; } + .spark .line { stroke-dashoffset: 0 !important; } + .spark .area { opacity: .09 !important; } +} diff --git a/resources/views/components/ui/metric.blade.php b/resources/views/components/ui/metric.blade.php new file mode 100644 index 0000000..491dc4f --- /dev/null +++ b/resources/views/components/ui/metric.blade.php @@ -0,0 +1,48 @@ +@props([ + 'label', + // The figure, and the unit that follows it in lighter weight — "235" and + // "/ 500 GB" read as one measurement rather than as two numbers. + 'value', + 'unit' => null, + 'foot' => null, + // A chevron to the page that explains the figure. Omitted where there is + // no such page: a chevron that goes nowhere is worse than none. + 'href' => null, +]) +{{-- + One metric card, exactly as the approved template draws it: small caps + label with an optional chevron, the figure in tabular monospace with its + unit beside it, a line of context underneath, and the visual on the right. + + The visual is a slot so the card does not have to know whether it is a + ring, a bar or a sparkline — and so a metric with nothing to draw simply + passes none instead of getting a decorative one. +--}} +
merge(['class' => 'rounded-lg border border-line bg-surface p-5 shadow-xs']) }}> +
+ {{ $label }} + @if ($href) + + + + @endif +
+ +
+
+

+ {{ $value }} + @if ($unit){{ $unit }}@endif +

+ @if ($foot) +

{{ $foot }}

+ @endif +
+ + @isset($visual) +
{{ $visual }}
+ @endisset +
+ + {{ $slot }} +
diff --git a/resources/views/components/ui/ring.blade.php b/resources/views/components/ui/ring.blade.php new file mode 100644 index 0000000..3a6115f --- /dev/null +++ b/resources/views/components/ui/ring.blade.php @@ -0,0 +1,14 @@ +@props(['percent' => 0, 'size' => 62]) +@php + // Circumference of r=25 in the 62-unit box. The offset is what is NOT + // filled, so a full ring is 0 and an empty one is the whole circumference. + $c = 157; + $o = (int) round($c * (1 - min(100, max(0, (float) $percent)) / 100)); +@endphp +
+ + {{ (int) round($percent) }}% +
diff --git a/resources/views/components/ui/spark.blade.php b/resources/views/components/ui/spark.blade.php new file mode 100644 index 0000000..cb5e788 --- /dev/null +++ b/resources/views/components/ui/spark.blade.php @@ -0,0 +1,31 @@ +@props([ + /** @var array the series, in the order it happened */ + 'points' => [], + // muted where the figure is observed, accent where it can be acted on. + // Four accent charts in a row is how an accent stops meaning anything. + 'tone' => 'accent', + 'area' => false, +]) +@php + $values = array_values(array_filter($points, 'is_numeric')); + $min = $values ? min($values) : 0; + $max = $values ? max($values) : 0; + $span = max(0.0001, $max - $min); + $step = count($values) > 1 ? 96 / (count($values) - 1) : 96; + + $path = ''; + foreach ($values as $i => $v) { + $x = round($i * $step, 1); + $y = round(30 - (($v - $min) / $span) * 26, 1); + $path .= ($i === 0 ? 'M' : ' ').$x.' '.$y; + } +@endphp +@if ($path !== '') + +@endif diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index ca91c7d..bc5cd5d 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -10,10 +10,10 @@ customer meets one vocabulary on both sides of the login. --}}
- +

{{ __('dashboard.sheet', ['when' => $asOf->locale($locale)->isoFormat('LL, LT')]) }} - -

+

+

{{ $instance !== null ? __('dashboard.title_running') : __('dashboard.title_pending') }}

@@ -53,40 +53,48 @@ @else - {{-- ── What the customer has ─────────────────────────────────────── --}} + {{-- ── What the customer has ───────────────────────────────────── + The template's metric card, and only the visuals there is data + for. Storage consumption is not measured yet, so that card states + the allowance instead of drawing a ring at an invented level — + this is the sheet a customer forwards to their auditor. --}}
@if ($instance->quota_gb) -
-

{{ __('dashboard.master.storage') }}

-

{{ __('dashboard.master.gb', ['n' => $instance->quota_gb]) }}

- {{-- 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. --}} -

{{ __('dashboard.master.storage_note') }}

-
+ @endif -
-

{{ __('dashboard.master.users') }}

-

- {{ $seats['total'] !== null - ? __('dashboard.master.of_total', ['used' => $seats['used'], 'total' => $seats['total']]) - : $seats['used'] }} -

+ @if ($seats['total']) -
- {{-- R4: width is data, not decoration. --}} -
-
+ + + @endif -
+
@if ($traffic) @php $state = $traffic->state(); @endphp -
-

{{ __('dashboard.traffic.title') }}

-

{{ Bytes::human($traffic->usedBytes) }}

-
+ +
+ {{-- R4: width is data, not decoration. --}}
$state === 'ok', @@ -95,26 +103,24 @@ ]) style="width: {{ min(100, $traffic->percent()) }}%">
-

{{ __('dashboard.traffic.of', ['total' => Bytes::human($traffic->quotaBytes)]) }}

@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. --}} - + {{ __('dashboard.traffic.buy') }} @endif -
+ @endif @if ($location) -
-

{{ __('dashboard.master.location') }}

-

{{ $location['name'] }}

- @if ($location['note']) -

{{ $location['note'] }}

- @endif -
+ @endif
diff --git a/resources/views/livewire/support.blade.php b/resources/views/livewire/support.blade.php index b74a947..162b87f 100644 --- a/resources/views/livewire/support.blade.php +++ b/resources/views/livewire/support.blade.php @@ -28,28 +28,6 @@ {{-- Tickets --}} -
-

{{ __('support.tickets') }}

-
- - - @foreach ($tickets as $t) - - - - - @endforeach - -
-

{{ $t['subject'] }}

-

{{ $t['ref'] }} · {{ $t['date'] }}

-
- {{ __('support.status_'.$t['status']) }} -
-
-
- - {{-- FAQ accordion --}}

{{ __('support.faq') }}

    diff --git a/resources/views/livewire/users.blade.php b/resources/views/livewire/users.blade.php index 93af148..32c7d8a 100644 --- a/resources/views/livewire/users.blade.php +++ b/resources/views/livewire/users.blade.php @@ -71,6 +71,9 @@
    + @if ($seat->role === 'owner') + + @endif @if ($seat->status === 'invited') @endif