Add account-level Analytics page to QR Plus.
Deploy Ladill QR Plus / deploy (push) Successful in 39s
Deploy Ladill QR Plus / deploy (push) Successful in 39s
Aggregates scan metrics across all codes with charts, breakdowns, and a sidebar nav item. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12 11.2 3.05c.44-.44 1.15-.44 1.59 0L21.75 12M4.5 9.75v10.5a.75.75 0 0 0 .75.75H9.75v-6a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75v6h4.5a.75.75 0 0 0 .75-.75V9.75" />'],
|
||||
['name' => 'My Codes', 'route' => route('user.qr-codes.index'), 'active' => request()->routeIs('user.qr-codes.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 3.75 9.375v-4.5ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 13.5 9.375v-4.5Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75ZM6.75 16.5h.75v.75h-.75v-.75ZM16.5 6.75h.75v.75h-.75v-.75ZM13.5 13.5h.75v.75h-.75v-.75ZM16.5 16.5h.75v.75h-.75v-.75ZM13.5 19.5h.75v.75h-.75v-.75ZM19.5 13.5h.75v.75h-.75v-.75ZM19.5 19.5h.75v.75h-.75v-.75ZM22.5 18.75a2.25 2.25 0 0 0-2.25-2.25h-1.5a2.25 2.25 0 0 0-2.25 2.25v1.5a2.25 2.25 0 0 0 2.25 2.25h1.5a2.25 2.25 0 0 0 2.25-2.25v-1.5Z" />'],
|
||||
['name' => 'Analytics', 'route' => route('qr.analytics.index'), 'active' => request()->routeIs('qr.analytics.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />'],
|
||||
];
|
||||
@endphp
|
||||
<nav class="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<x-user-layout>
|
||||
<x-slot name="title">Analytics</x-slot>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Analytics</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Scan activity across all your QR Plus codes.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||||
<p class="text-2xl font-bold text-slate-900">{{ number_format($summary['total_scans']) }}</p>
|
||||
<p class="mt-0.5 text-xs text-slate-400">Total scans</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||||
<p class="text-2xl font-bold text-slate-900">{{ number_format($summary['unique_scans']) }}</p>
|
||||
<p class="mt-0.5 text-xs text-slate-400">Unique scans</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||||
<p class="text-2xl font-bold text-slate-900">{{ number_format($summary['scans_7d']) }}</p>
|
||||
<p class="mt-0.5 text-xs text-slate-400">Last 7 days</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||||
<p class="text-2xl font-bold text-slate-900">{{ number_format($summary['scans_30d']) }}</p>
|
||||
<p class="mt-0.5 text-xs text-slate-400">Last 30 days</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200/80 bg-white p-6 shadow-sm">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Scans — last 30 days</h2>
|
||||
@php $maxDaily = max(1, $dailyScans->max('total')); @endphp
|
||||
<div class="mt-5 flex h-28 items-end gap-px">
|
||||
@foreach($dailyScans as $day)
|
||||
<div class="group relative flex flex-1 flex-col items-center justify-end"
|
||||
title="{{ $day->date }}: {{ $day->total }}">
|
||||
<div class="w-full rounded-t-sm bg-indigo-400/70 transition-colors group-hover:bg-indigo-600"
|
||||
style="height: {{ max(2, ($day->total / $maxDaily) * 100) }}%"></div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Devices</h2>
|
||||
<ul class="mt-4 space-y-2.5">
|
||||
@forelse($devices as $row)
|
||||
<li class="flex items-center justify-between text-sm">
|
||||
<span class="text-slate-600">{{ ucfirst($row['label']) }}</span>
|
||||
<span class="font-semibold text-slate-900">{{ number_format($row['total']) }}</span>
|
||||
</li>
|
||||
@empty
|
||||
<li class="text-sm text-slate-400">No scans yet</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Browsers</h2>
|
||||
<ul class="mt-4 space-y-2.5">
|
||||
@forelse($browsers as $row)
|
||||
<li class="flex items-center justify-between text-sm">
|
||||
<span class="text-slate-600">{{ $row['label'] }}</span>
|
||||
<span class="font-semibold text-slate-900">{{ number_format($row['total']) }}</span>
|
||||
</li>
|
||||
@empty
|
||||
<li class="text-sm text-slate-400">No scans yet</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white">
|
||||
<div class="border-b border-slate-100 px-6 py-4">
|
||||
<h2 class="font-semibold text-slate-900">Top codes</h2>
|
||||
</div>
|
||||
@if($topCodes->isEmpty())
|
||||
<p class="px-6 py-8 text-sm text-slate-500">No scan data yet.</p>
|
||||
@else
|
||||
<div class="divide-y divide-slate-100">
|
||||
@foreach($topCodes as $code)
|
||||
<div class="flex items-center justify-between px-6 py-4">
|
||||
<a href="{{ route('user.qr-codes.show', $code) }}" class="font-medium text-indigo-600 hover:text-indigo-800">{{ $code->label }}</a>
|
||||
<span class="text-sm text-slate-600">{{ number_format($code->scans_total) }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white">
|
||||
<div class="border-b border-slate-100 px-6 py-4">
|
||||
<h2 class="font-semibold text-slate-900">Recent scans</h2>
|
||||
</div>
|
||||
@if($recentScans->isEmpty())
|
||||
<p class="px-6 py-8 text-sm text-slate-500">No scans recorded yet.</p>
|
||||
@else
|
||||
<div class="divide-y divide-slate-100">
|
||||
@foreach($recentScans as $event)
|
||||
<div class="px-6 py-4">
|
||||
<p class="text-sm font-medium text-slate-900">{{ $event->qrCode?->label ?? 'QR code' }}</p>
|
||||
<p class="text-xs text-slate-500">
|
||||
{{ ucfirst($event->device_type ?? 'unknown') }}
|
||||
@if($event->browser)
|
||||
· {{ $event->browser }}
|
||||
@endif
|
||||
· {{ $event->scanned_at->diffForHumans() }}
|
||||
</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-user-layout>
|
||||
Reference in New Issue
Block a user