diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 6bd12e7..07e75ec 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -2,6 +2,8 @@ namespace App\Livewire; +use Illuminate\Support\Carbon; +use Illuminate\Support\Number; use Livewire\Attributes\Layout; use Livewire\Component; @@ -10,34 +12,111 @@ class Dashboard extends Component { public function render() { - // Fixture data until the provisioning engine (separate handoff) provides - // real instances / provisioning_runs. The dashboard is a pure view (§12.3). - $storageUsed = 172; - $storageQuota = 250; + // Locale-aware date / number formatting for the fixture values below. + $locale = app()->getLocale(); + $date = fn (string $iso, string $fmt) => Carbon::parse($iso)->locale($locale)->isoFormat($fmt); + $gb = fn (float $v) => Number::format($v, precision: 1, locale: $locale).' GB'; + // Fixture data until the provisioning engine (separate handoff) supplies + // real instances / metrics. Tenant-specific values stand in for DB rows. + $storageUsed = 235; + $storageQuota = 500; $storagePercent = (int) round($storageUsed / max(1, $storageQuota) * 100); - $storageTone = $storagePercent >= 90 ? 'danger' : ($storagePercent >= 85 ? 'warning' : 'accent'); + + // ~30 days of availability, gently trending near 100%. + $availability = [99.95, 99.97, 99.96, 99.98, 99.99, 99.97, 99.98, 99.99, 99.99, 99.98, + 99.97, 99.99, 100, 99.98, 99.99, 99.99, 99.97, 99.98, 99.99, 100, + 99.98, 99.99, 99.99, 99.98, 100, 99.99, 99.98, 99.99, 100, 99.98]; return view('livewire.dashboard', [ 'storageUsed' => $storageUsed, 'storageQuota' => $storageQuota, 'storagePercent' => $storagePercent, - 'storageTone' => $storageTone, - 'activeUsers' => 14, - 'lastBackup' => __('dashboard.today_time', ['time' => '04:12']), - 'uptime' => '99.98', - 'instanceUrl' => 'https://kunde-demo.clupilot.com', - 'instanceStatus' => 'active', - 'provisioning' => false, - 'steps' => [ - ['label' => __('dashboard.steps.validate'), 'state' => 'done'], - ['label' => __('dashboard.steps.provision'), 'state' => 'done'], - ['label' => __('dashboard.steps.configure'), 'state' => 'done'], - ['label' => __('dashboard.steps.acceptance'), 'state' => 'done'], + 'usersActive' => 8, + 'usersQuota' => 20, + 'lastBackup' => '03:12', + 'availability' => Number::format(99.98, precision: 2, locale: $locale), + + 'storageChart' => [ + 'type' => 'doughnut', + 'data' => [ + 'datasets' => [[ + 'data' => [$storagePercent, 100 - $storagePercent], + 'backgroundColor' => ['token:accent', 'token:surface-2'], + 'borderWidth' => 0, + ]], + ], + 'options' => [ + 'cutout' => '76%', + 'plugins' => [ + 'legend' => ['display' => false], + 'tooltip' => ['enabled' => false], + ], + ], ], + + 'availabilityChart' => [ + 'type' => 'line', + 'data' => [ + 'labels' => array_fill(0, count($availability), ''), + 'datasets' => [[ + 'data' => $availability, + 'borderColor' => 'token:success-bright', + 'backgroundColor' => 'token:success-bright/0.12', + 'fill' => true, + 'tension' => 0.4, + 'pointRadius' => 0, + 'borderWidth' => 2, + ]], + ], + 'options' => [ + 'scales' => [ + 'x' => ['display' => false], + 'y' => ['display' => false, 'min' => 99.9, 'max' => 100], + ], + 'plugins' => [ + 'legend' => ['display' => false], + 'tooltip' => ['enabled' => false], + ], + ], + ], + + 'cloud' => [ + 'name' => 'Cloud der Kanzlei Berger', + 'domain' => 'cloud.kanzlei-berger.at', + 'package' => __('dashboard.cloud.plan_line', ['plan' => 'CluPilot Cloud Team', 'price' => 179]), + 'since' => $date('2026-03-14', 'LL'), + 'location' => __('dashboard.cloud.datacenter'), + 'version' => 'Nextcloud 31.0.4', + ], + + 'onboarding' => [ + ['label' => __('dashboard.onboarding.instance'), 'done' => true, 'date' => $date('2026-03-14', 'D. MMMM')], + ['label' => __('dashboard.onboarding.domain'), 'done' => true, 'date' => $date('2026-03-15', 'D. MMMM')], + ['label' => __('dashboard.onboarding.branding'), 'done' => true, 'date' => $date('2026-03-15', 'D. MMMM')], + ['label' => __('dashboard.onboarding.users'), 'done' => true, 'date' => $date('2026-03-16', 'D. MMMM')], + ['label' => __('dashboard.onboarding.migration'), 'done' => false, 'date' => null], + ['label' => __('dashboard.onboarding.training'), 'done' => false, 'date' => null], + ['label' => __('dashboard.onboarding.docs'), 'done' => false, 'date' => null], + ], + + 'backups' => [ + ['when' => __('dashboard.today').', 03:12', 'size' => $gb(18.4)], + ['when' => __('dashboard.yesterday').', 03:09', 'size' => $gb(18.4)], + ['when' => $date('2026-07-21', 'D. MMMM').', 03:11', 'size' => $gb(18.3)], + ], + + 'modules' => [ + ['icon' => 'cloud', 'name' => 'Nextcloud + Collabora', 'desc' => __('dashboard.modules.nextcloud'), 'status' => 'active'], + ['icon' => 'shield-check', 'name' => 'Vaultwarden', 'desc' => __('dashboard.modules.vaultwarden'), 'status' => 'active'], + ['icon' => 'receipt', 'name' => 'Paperless-ngx', 'desc' => __('dashboard.modules.paperless'), 'status' => 'available'], + ['icon' => 'pen', 'name' => __('dashboard.modules.signature_name'), 'desc' => __('dashboard.modules.signature'), 'status' => 'soon'], + ], + 'activity' => [ - ['label' => __('dashboard.activity_items.backup_ok'), 'time' => '04:12'], - ['label' => __('dashboard.activity_items.user_added'), 'time' => __('dashboard.yesterday')], - ['label' => __('dashboard.activity_items.login'), 'time' => __('dashboard.yesterday')], + ['icon' => 'download', 'text' => __('dashboard.activity_items.backup_ok'), 'time' => '03:12'], + ['icon' => 'shield-check', 'text' => __('dashboard.activity_items.security'), 'time' => '04:01'], + ['icon' => 'users', 'text' => __('dashboard.activity_items.upload'), 'time' => '07:56'], + ['icon' => 'shield-check', 'text' => __('dashboard.activity_items.cert'), 'time' => __('dashboard.yesterday').' 22:10'], ], ]); } diff --git a/lang/de/dashboard.php b/lang/de/dashboard.php index 63512e5..d2102b3 100644 --- a/lang/de/dashboard.php +++ b/lang/de/dashboard.php @@ -3,11 +3,14 @@ return [ 'title' => 'Übersicht', 'subtitle' => 'Status Ihrer Cloud auf einen Blick.', + 'greeting' => 'Guten Morgen, :name', + 'system_ok' => 'Alle Systeme normal', + 'as_of' => 'Zustand Ihrer Cloud auf einen Blick — Stand heute, :time Uhr.', 'nav' => [ 'overview' => 'Übersicht', 'cloud' => 'Meine Cloud', - 'users' => 'Nutzer', + 'users' => 'Benutzer', 'backups' => 'Backups', 'invoices' => 'Rechnungen', 'support' => 'Support', @@ -17,19 +20,84 @@ return [ 'close_nav' => 'Navigation schließen', 'logout' => 'Abmelden', + 'today' => 'Heute', + 'yesterday' => 'Gestern', + 'kpi' => [ 'storage' => 'Speicher', - 'users' => 'Aktive Nutzer', + 'users' => 'Benutzer', 'backup' => 'Letztes Backup', - 'uptime' => 'Verfügbarkeit', + 'availability' => 'Verfügbarkeit · 30 Tage', + ], + 'occupied' => 'belegt', + 'storage_week' => '+3,4 GB diese Woche', + 'users_detail' => '3 Gruppen · 2 Gastzugänge', + 'backup_verified' => 'heute · verschlüsselt & geprüft', + + 'upsell' => [ + 'title' => 'Ihr Speicher ist zu :percent % belegt.', + 'body' => 'Jederzeit erweiterbar — +100 GB für 10 € pro Monat, monatlich kündbar.', + 'cta' => 'Speicher erweitern', + ], + + 'cloud' => [ + 'title' => 'Meine Cloud', + 'open' => 'Cloud öffnen', + 'package' => 'Paket', + 'status' => 'Status', + 'location' => 'Serverstandort', + 'version' => 'Version', + 'active_since' => 'Aktiv seit :date', + 'current' => 'aktuell', + 'plan_line' => ':plan · :price €/Monat', + 'datacenter' => 'EU · Rechenzentrum Falkenstein', + ], + + 'onboarding' => [ + 'title' => 'Einrichtung Ihrer Cloud', + 'progress' => ':done von :total erledigt', + 'review' => 'Jetzt prüfen', + 'next' => 'Weiter', + 'done_toast' => 'Erledigt — schöner Fortschritt!', + 'instance' => 'Instanz erstellt & gestartet', + 'domain' => 'Eigene Domain verbunden', + 'branding' => 'Kanzlei-Logo & Farben hinterlegt', + 'users' => '8 Benutzer angelegt, Rechte gesetzt', + 'migration' => 'Datenübernahme aus Dropbox freigeben', + 'training' => 'Einschulungstermin wählen', + 'docs' => 'Dokumentationsmappe herunterladen', + ], + + 'backups' => [ + 'title' => 'Backups', + 'schedule' => 'täglich 03:00', + 'restore' => 'Wiederherstellen', + 'note' => 'Clientseitig verschlüsselt, getrennt gelagert. Letzter Wiederherstellungstest: 01.07. — erfolgreich.', + 'restore_toast' => 'Wiederherstellung: wir melden uns binnen 1 Std.', + ], + + 'modules' => [ + 'title' => 'Ihre Module', + 'addon' => 'zubuchbar', + 'active' => 'Aktiv', + 'add' => 'Hinzubuchen', + 'soon' => 'In Vorbereitung', + 'nextcloud' => 'Dateien, Office, Kalender, Talk', + 'vaultwarden' => 'Team-Passwortmanager · Zusatzmodul 19 €/Monat', + 'paperless' => 'Digitales Dokumentenarchiv · ab 39 €/Monat', + 'signature_name' => 'Digitale Signatur', + 'signature' => 'Rechtsgültige Unterschriften · ab 29 €/Monat', + 'add_toast' => 'Zusatzmodul zubuchbar — wir beraten Sie gern.', ], - 'instance' => 'Instanz', - 'open_cloud' => 'Cloud öffnen', - 'provisioning' => 'Bereitstellung', 'activity' => 'Aktivität', - 'today_time' => 'Heute :time', - 'yesterday' => 'Gestern', + 'activity_live' => 'live', + 'activity_items' => [ + 'backup_ok' => 'Backup abgeschlossen — 18,4 GB', + 'security' => 'Sicherheitsupdates eingespielt', + 'upload' => 'M. Huber hat 14 Dateien hochgeladen', + 'cert' => 'SSL-Zertifikat automatisch erneuert', + ], 'status' => [ 'active' => 'Aktiv', @@ -38,23 +106,8 @@ return [ 'failed' => 'Fehlgeschlagen', ], - 'steps' => [ - 'validate' => 'Bestellung geprüft', - 'provision' => 'Instanz erstellt', - 'configure' => 'Cloud konfiguriert', - 'acceptance' => 'Abnahme bestanden', - ], - - 'step_state' => [ - 'pending' => 'Ausstehend', - 'running' => 'Läuft', - 'done' => 'Abgeschlossen', - 'failed' => 'Fehlgeschlagen', - ], - - 'activity_items' => [ - 'backup_ok' => 'Backup erfolgreich abgeschlossen', - 'user_added' => 'Neuer Nutzer hinzugefügt', - 'login' => 'Anmeldung am Portal', - ], + 'instance' => 'Instanz', + 'open_cloud' => 'Cloud öffnen', + 'provisioning' => 'Bereitstellung', + 'today_time' => 'Heute :time', ]; diff --git a/lang/en/dashboard.php b/lang/en/dashboard.php index bc70a59..9578428 100644 --- a/lang/en/dashboard.php +++ b/lang/en/dashboard.php @@ -3,6 +3,9 @@ return [ 'title' => 'Overview', 'subtitle' => 'Your cloud status at a glance.', + 'greeting' => 'Good morning, :name', + 'system_ok' => 'All systems normal', + 'as_of' => 'Your cloud status at a glance — as of today, :time.', 'nav' => [ 'overview' => 'Overview', @@ -17,19 +20,84 @@ return [ 'close_nav' => 'Close navigation', 'logout' => 'Sign out', + 'today' => 'Today', + 'yesterday' => 'Yesterday', + 'kpi' => [ 'storage' => 'Storage', - 'users' => 'Active users', + 'users' => 'Users', 'backup' => 'Last backup', - 'uptime' => 'Uptime', + 'availability' => 'Uptime · 30 days', + ], + 'occupied' => 'used', + 'storage_week' => '+3.4 GB this week', + 'users_detail' => '3 groups · 2 guest accounts', + 'backup_verified' => 'today · encrypted & verified', + + 'upsell' => [ + 'title' => 'Your storage is :percent% used.', + 'body' => 'Expandable any time — +100 GB for €10 per month, cancel monthly.', + 'cta' => 'Add storage', + ], + + 'cloud' => [ + 'title' => 'My cloud', + 'open' => 'Open cloud', + 'package' => 'Plan', + 'status' => 'Status', + 'location' => 'Server location', + 'version' => 'Version', + 'active_since' => 'Active since :date', + 'current' => 'up to date', + 'plan_line' => ':plan · €:price/mo', + 'datacenter' => 'EU · Falkenstein data center', + ], + + 'onboarding' => [ + 'title' => 'Setting up your cloud', + 'progress' => ':done of :total done', + 'review' => 'Review now', + 'next' => 'Next', + 'done_toast' => 'Done — nice progress!', + 'instance' => 'Instance created & started', + 'domain' => 'Custom domain connected', + 'branding' => 'Firm logo & colors applied', + 'users' => '8 users created, permissions set', + 'migration' => 'Approve data import from Dropbox', + 'training' => 'Choose a training appointment', + 'docs' => 'Download the documentation pack', + ], + + 'backups' => [ + 'title' => 'Backups', + 'schedule' => 'daily 03:00', + 'restore' => 'Restore', + 'note' => 'Client-side encrypted, stored separately. Last restore test: 01 Jul — successful.', + 'restore_toast' => 'Restore: we will get back to you within 1 hr.', + ], + + 'modules' => [ + 'title' => 'Your modules', + 'addon' => 'add-on', + 'active' => 'Active', + 'add' => 'Add', + 'soon' => 'Coming soon', + 'nextcloud' => 'Files, office, calendar, Talk', + 'vaultwarden' => 'Team password manager · add-on €19/month', + 'paperless' => 'Digital document archive · from €39/month', + 'signature_name' => 'Digital signature', + 'signature' => 'Legally binding signatures · from €29/month', + 'add_toast' => 'Add-on available — we are happy to advise you.', ], - 'instance' => 'Instance', - 'open_cloud' => 'Open cloud', - 'provisioning' => 'Provisioning', 'activity' => 'Activity', - 'today_time' => 'Today :time', - 'yesterday' => 'Yesterday', + 'activity_live' => 'live', + 'activity_items' => [ + 'backup_ok' => 'Backup completed — 18.4 GB', + 'security' => 'Security updates applied', + 'upload' => 'M. Huber uploaded 14 files', + 'cert' => 'SSL certificate auto-renewed', + ], 'status' => [ 'active' => 'Active', @@ -38,23 +106,8 @@ return [ 'failed' => 'Failed', ], - 'steps' => [ - 'validate' => 'Order validated', - 'provision' => 'Instance created', - 'configure' => 'Cloud configured', - 'acceptance' => 'Acceptance passed', - ], - - 'step_state' => [ - 'pending' => 'Pending', - 'running' => 'Running', - 'done' => 'Done', - 'failed' => 'Failed', - ], - - 'activity_items' => [ - 'backup_ok' => 'Backup completed successfully', - 'user_added' => 'New user added', - 'login' => 'Portal sign-in', - ], + 'instance' => 'Instance', + 'open_cloud' => 'Open cloud', + 'provisioning' => 'Provisioning', + 'today_time' => 'Today :time', ]; diff --git a/package-lock.json b/package-lock.json index 7ee4178..19aede1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "dependencies": { "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", + "chart.js": "^4.5.1", "laravel-echo": "^2.4.0", "pusher-js": "^8.6.0" }, @@ -123,6 +124,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -703,6 +710,18 @@ "node": ">=8" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", diff --git a/package.json b/package.json index 0937a52..5804ed6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", + "chart.js": "^4.5.1", "laravel-echo": "^2.4.0", "pusher-js": "^8.6.0" } diff --git a/resources/css/portal-tokens.css b/resources/css/portal-tokens.css index f7c107a..b1b9267 100644 --- a/resources/css/portal-tokens.css +++ b/resources/css/portal-tokens.css @@ -33,6 +33,7 @@ /* Status semantics — only in badges/alerts, never a second decorative tone */ --success: #067a48; --success-bg: #ecfdf3; --success-border: #abefc6; + --success-bright: #30b15c; /* dots/rings/charts only (large marks, not small text) */ --warning: #b54708; --warning-bg: #fffaeb; --warning-border: #fedf89; --danger: #b42318; --danger-bg: #fef3f2; --danger-border: #fecdca; --info: #175cd3; --info-bg: #eff8ff; --info-border: #b2ddff; @@ -49,7 +50,7 @@ --tracking-label: 0.06em; /* Radius / shadow / motion / focus */ - --radius-sm: 6px; --radius: 8px; --radius-lg: 12px; --radius-pill: 999px; + --radius-sm: 6px; --radius: 8px; --radius-lg: 12px; --radius-xl: 20px; --radius-pill: 999px; --shadow-xs: 0 1px 2px rgba(16, 24, 40, .05); --shadow-sm: 0 1px 3px rgba(16, 24, 40, .10), 0 1px 2px rgba(16, 24, 40, .06); diff --git a/resources/js/app.js b/resources/js/app.js index 6a56c80..f194162 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,49 +1,95 @@ import './echo'; +import { Chart, registerables } from 'chart.js'; + +Chart.register(...registerables); + +const prefersReducedMotion = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +// Resolve "token:name" / "token:name/0.12" strings in a chart config to real +// colours pulled from the CSS design tokens (keeps charts on-palette, R3). +function tokenColor(spec, css) { + const [name, alpha] = spec.split('/'); + const value = css.getPropertyValue('--' + name.trim()).trim(); + if (!value) return spec; + if (alpha && value.startsWith('#')) { + let hex = value.slice(1); + if (hex.length === 3) hex = hex.split('').map((c) => c + c).join(''); + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + } + return value; +} + +function resolveTokens(node, css) { + if (typeof node === 'string') return node.startsWith('token:') ? tokenColor(node.slice(6), css) : node; + if (Array.isArray(node)) return node.map((n) => resolveTokens(n, css)); + if (node && typeof node === 'object') { + const out = {}; + for (const key of Object.keys(node)) out[key] = resolveTokens(node[key], css); + return out; + } + return node; +} -// Segmented OTP input behaviour (used by ). Alpine ships with -// Livewire; register on alpine:init. document.addEventListener('alpine:init', () => { + // ── Segmented OTP input ────────────────────────────────────────────── window.Alpine.data('otpInput', ({ length = 6 } = {}) => ({ length, digits: Array(length).fill(''), - get value() { return this.digits.join(''); }, - cells() { return this.$root.querySelectorAll('input[inputmode="numeric"]'); }, - focusCell(i) { const cell = this.cells()[i]; if (cell) cell.focus(); }, - onInput(i, e) { const digit = e.target.value.replace(/\D/g, '').slice(-1); this.digits[i] = digit; if (digit && i < this.length - 1) this.focusCell(i + 1); this.maybeSubmit(); }, - onBackspace(i) { if (!this.digits[i] && i > 0) this.focusCell(i - 1); }, - onPaste(e) { const text = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, this.length); for (let i = 0; i < this.length; i++) this.digits[i] = text[i] || ''; this.focusCell(Math.min(text.length, this.length - 1)); this.maybeSubmit(); }, - maybeSubmit() { if (this.value.length === this.length) { - // Wait for Alpine to flush :value into the hidden input, otherwise - // requestSubmit() can post a stale/short code. this.$nextTick(() => this.$root.closest('form')?.requestSubmit()); } }, })); + + // ── Chart.js island ────────────────────────────────────────────────── + window.Alpine.data('chart', (config) => ({ + instance: null, + init() { + const css = getComputedStyle(document.documentElement); + Chart.defaults.font.family = css.getPropertyValue('--font-sans').trim() || 'sans-serif'; + Chart.defaults.color = css.getPropertyValue('--text-muted').trim() || '#667085'; + Chart.defaults.borderColor = css.getPropertyValue('--border').trim() || '#e4e7ec'; + + const cfg = resolveTokens(config, css); + cfg.options = { + responsive: true, + maintainAspectRatio: false, + animation: prefersReducedMotion() ? false : cfg.options?.animation, + ...cfg.options, + }; + this.instance = new Chart(this.$refs.canvas, cfg); + }, + destroy() { + this.instance?.destroy(); + }, + })); }); diff --git a/resources/views/components/ui/chart.blade.php b/resources/views/components/ui/chart.blade.php new file mode 100644 index 0000000..7141946 --- /dev/null +++ b/resources/views/components/ui/chart.blade.php @@ -0,0 +1,13 @@ +@props([ + // Chart.js config as a PHP array. Colours may use "token:accent" / + // "token:accent/0.12" strings, resolved from CSS design tokens at runtime. + 'config', + 'label' => null, +]) +
merge(['class' => 'relative']) }} +> + +
diff --git a/resources/views/components/ui/icon.blade.php b/resources/views/components/ui/icon.blade.php index 74e7081..d2580f6 100644 --- a/resources/views/components/ui/icon.blade.php +++ b/resources/views/components/ui/icon.blade.php @@ -13,6 +13,12 @@ 'chevron-down' => '', 'external-link'=> '', 'shield-check' => '', + 'shield' => '', + 'pen' => '', + 'download' => '', + 'alert-triangle' => '', + 'check' => '', + 'plus' => '', ]; $body = $icons[$name] ?? ''; @endphp diff --git a/resources/views/layouts/portal-app.blade.php b/resources/views/layouts/portal-app.blade.php index 3c93ebf..07422b9 100644 --- a/resources/views/layouts/portal-app.blade.php +++ b/resources/views/layouts/portal-app.blade.php @@ -85,6 +85,20 @@ + {{-- Global toast — any element can trigger it with $dispatch('notify', { message }) --}} +
+ +
+ @livewireScripts diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index e7f54ba..7574f3b 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -1,61 +1,212 @@ -
-
-

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

-

{{ __('dashboard.subtitle') }}

+
+ {{-- Header --}} +
+

{{ __('dashboard.greeting', ['name' => auth()->user()->name]) }}

+ + {{ __('dashboard.system_ok') }} + +

{{ __('dashboard.as_of', ['time' => '08:42']) }}

- {{-- KPI row: 1 → 2 → 4 (R7) --}} + {{-- KPI row --}}
- - - - -
- -
- {{-- Instance card --}} - -
-
-

{{ __('dashboard.instance') }}

-

{{ $instanceUrl }}

-
- {{ __('dashboard.status.'.$instanceStatus) }} + {{-- Storage ring --}} +
+
+ +
+
+ {{ $storagePercent }}% + {{ __('dashboard.occupied') }}
- - - {{ __('dashboard.open_cloud') }} -
- +
+

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

+

{{ $storageUsed }}/ {{ $storageQuota }} GB

+

{{ __('dashboard.storage_week') }}

+
+
- {{-- Provisioning progress (live via Reverb once the engine lands) --}} - - - + {{-- Users --}} +
+

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

+

{{ $usersActive }}/ {{ $usersQuota }}

+

{{ __('dashboard.users_detail') }}

+
+ + {{-- Last backup --}} +
+

{{ __('dashboard.kpi.backup') }}

+

+ {{ $lastBackup }} +

+

{{ __('dashboard.backup_verified') }}

+
+ + {{-- Availability sparkline --}} +
+

{{ __('dashboard.kpi.availability') }}

+

{{ $availability }}%

+
+ +
+
- {{-- Activity --}} - -
    - @foreach ($activity as $item) -
  • - {{ $item['label'] }} - {{ $item['time'] }} -
  • - @endforeach -
-
+ {{-- Upsell --}} +
+ +
+

{{ __('dashboard.upsell.title', ['percent' => $storagePercent]) }}

+

{{ __('dashboard.upsell.body') }}

+
+ + {{ __('dashboard.upsell.cta') }} + +
+ +
+ {{-- Left column --}} +
+ {{-- Cloud card --}} +
+ +
+

{{ __('dashboard.cloud.package') }}

{{ $cloud['package'] }}

+

{{ __('dashboard.cloud.status') }}

{{ __('dashboard.cloud.active_since', ['date' => $cloud['since']]) }}

+

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

{{ $cloud['location'] }}

+

{{ __('dashboard.cloud.version') }}

{{ $cloud['version'] }} · {{ __('dashboard.cloud.current') }}

+
+
+ + {{-- Onboarding checklist (Alpine) --}} +
+
+

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

+ +
+
+
+
+
    + +
+
+
+ + {{-- Right column --}} +
+ {{-- Backups --}} +
+
+

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

+ {{ __('dashboard.backups.schedule') }} +
+
    + @foreach ($backups as $b) +
  • + + {{ $b['when'] }} + {{ $b['size'] }} + {{ __('dashboard.backups.restore') }} +
  • + @endforeach +
+

+ + {{ __('dashboard.backups.note') }} +

+
+ + {{-- Modules --}} +
+
+

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

+ {{ __('dashboard.modules.addon') }} +
+
    + @foreach ($modules as $m) +
  • + +
    +

    {{ $m['name'] }}

    +

    {{ $m['desc'] }}

    +
    + @if ($m['status'] === 'active') + {{ __('dashboard.modules.active') }} + @elseif ($m['status'] === 'available') + {{ __('dashboard.modules.add') }} + @else + {{ __('dashboard.modules.soon') }} + @endif +
  • + @endforeach +
+
+ + {{-- Activity feed --}} +
+
+

{{ __('dashboard.activity') }}

+ {{ __('dashboard.activity_live') }} +
+
    + @foreach ($activity as $a) +
  • + +
    +

    {{ $a['text'] }}

    +

    {{ $a['time'] }}

    +
    +
  • + @endforeach +
+
+
+
diff --git a/tailwind.config.js b/tailwind.config.js index 0c7aa5e..7057e3d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -34,7 +34,7 @@ export default { text: 'var(--accent-text)', }, 'on-accent': 'var(--on-accent)', - success: { DEFAULT: 'var(--success)', bg: 'var(--success-bg)', border: 'var(--success-border)' }, + success: { DEFAULT: 'var(--success)', bg: 'var(--success-bg)', border: 'var(--success-border)', bright: 'var(--success-bright)' }, warning: { DEFAULT: 'var(--warning)', bg: 'var(--warning-bg)', border: 'var(--warning-border)' }, danger: { DEFAULT: 'var(--danger)', bg: 'var(--danger-bg)', border: 'var(--danger-border)' }, info: { DEFAULT: 'var(--info)', bg: 'var(--info-bg)', border: 'var(--info-border)' }, @@ -48,12 +48,21 @@ export default { lg: 'var(--text-lg)', xl: 'var(--text-xl)', '2xl': 'var(--text-2xl)', '3xl': 'var(--text-3xl)', }, borderRadius: { - sm: 'var(--radius-sm)', DEFAULT: 'var(--radius)', lg: 'var(--radius-lg)', pill: 'var(--radius-pill)', + sm: 'var(--radius-sm)', DEFAULT: 'var(--radius)', lg: 'var(--radius-lg)', xl: 'var(--radius-xl)', pill: 'var(--radius-pill)', }, boxShadow: { xs: 'var(--shadow-xs)', sm: 'var(--shadow-sm)', md: 'var(--shadow-md)', }, ringColor: { accent: 'var(--accent-ring)' }, + keyframes: { + rise: { + '0%': { opacity: '0', transform: 'translateY(16px)' }, + '100%': { opacity: '1', transform: 'none' }, + }, + }, + animation: { + rise: 'rise 0.6s cubic-bezier(.2,.7,.2,1) both', + }, }, }, plugins: [],