feat: add tag discovery analytics and reporting
This commit is contained in:
@@ -2,9 +2,23 @@
|
||||
|
||||
@section('content')
|
||||
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8 py-10">
|
||||
<div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-6">
|
||||
<h1 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Reports Queue</h1>
|
||||
<p class="mt-2 text-sm text-gray-500">Use the API endpoint <code>/api/reports</code> to submit reports and review records in the <code>reports</code> table.</p>
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-6">
|
||||
<h1 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Reports Hub</h1>
|
||||
<p class="mt-2 text-sm text-gray-500">Internal reporting entry points for moderation and discovery analytics.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<a href="{{ route('admin.reports.tags') }}" class="rounded-xl border border-sky-200 bg-sky-50 p-6 transition hover:border-sky-300 hover:bg-sky-100/80 dark:border-sky-900/60 dark:bg-sky-950/30 dark:hover:border-sky-700 dark:hover:bg-sky-950/50">
|
||||
<h2 class="text-lg font-semibold text-slate-900 dark:text-sky-100">Tag Interaction Report</h2>
|
||||
<p class="mt-2 text-sm text-slate-600 dark:text-sky-200/70">Inspect top surfaces, tags, search terms, and related-tag transitions from the new tag analytics pipeline.</p>
|
||||
</a>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Moderation Queue</h2>
|
||||
<p class="mt-2 text-sm text-gray-500">Use the API endpoint <code>/api/reports</code> to submit reports and review records in the <code>reports</code> table.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
216
resources/views/admin/reports/tags.blade.php
Normal file
216
resources/views/admin/reports/tags.blade.php
Normal file
@@ -0,0 +1,216 @@
|
||||
@extends('layouts.nova.content-layout')
|
||||
|
||||
@section('page-content')
|
||||
<div class="mx-auto max-w-7xl space-y-8">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Tag Interaction Report</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-neutral-400">
|
||||
Internal dashboard for tag discovery clicks. Use it to inspect surface performance, top tags, query demand, and tag-to-tag transitions for recommendation tuning.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3 text-xs">
|
||||
<a href="{{ route('api.admin.reports.tags', request()->query()) }}" class="inline-flex items-center rounded-lg border border-neutral-700 bg-neutral-900 px-3 py-2 font-medium text-neutral-200 transition hover:border-sky-500 hover:text-white">JSON report</a>
|
||||
<a href="{{ route('admin.reports.queue') }}" class="inline-flex items-center rounded-lg border border-neutral-700 bg-neutral-900 px-3 py-2 font-medium text-neutral-200 transition hover:border-sky-500 hover:text-white">Reports hub</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<form method="GET" action="{{ route('admin.reports.tags') }}" class="grid gap-4 md:grid-cols-4">
|
||||
<label class="space-y-2 text-sm text-neutral-300">
|
||||
<span class="block text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">From</span>
|
||||
<input type="date" name="from" value="{{ $filters['from'] }}" class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-white focus:border-sky-500 focus:outline-none">
|
||||
</label>
|
||||
<label class="space-y-2 text-sm text-neutral-300">
|
||||
<span class="block text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">To</span>
|
||||
<input type="date" name="to" value="{{ $filters['to'] }}" class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-white focus:border-sky-500 focus:outline-none">
|
||||
</label>
|
||||
<label class="space-y-2 text-sm text-neutral-300">
|
||||
<span class="block text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">Row limit</span>
|
||||
<input type="number" min="1" max="100" name="limit" value="{{ $filters['limit'] }}" class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-white focus:border-sky-500 focus:outline-none">
|
||||
</label>
|
||||
<div class="flex items-end gap-3">
|
||||
<button type="submit" class="inline-flex items-center rounded-lg bg-sky-500 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:bg-sky-400">Refresh</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-4 text-xs text-neutral-500">
|
||||
<span>Latest aggregated date: <span class="font-medium text-neutral-300">{{ $latestAggregatedDate ?? 'not aggregated yet' }}</span></span>
|
||||
<span>Latest raw event: <span class="font-medium text-neutral-300">{{ $overview['latest_event_at'] ?? 'n/a' }}</span></span>
|
||||
</div>
|
||||
|
||||
@if(app()->environment('local'))
|
||||
<div class="mt-4 rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-100">
|
||||
<p class="font-semibold">Local demo data</p>
|
||||
<p class="mt-1 text-amber-100/80">
|
||||
This report can be filled locally with seeded click data. Run
|
||||
<code class="rounded bg-black/30 px-2 py-1 text-xs text-amber-50">php artisan analytics:seed-tag-interaction-demo --days=14 --per-day=80 --refresh</code>
|
||||
and refresh this page to inspect realistic search, recommendation, and transition metrics.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">Total clicks</p>
|
||||
<p class="mt-3 text-3xl font-bold text-white">{{ number_format($overview['total_clicks']) }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">Unique users</p>
|
||||
<p class="mt-3 text-3xl font-bold text-white">{{ number_format($overview['unique_users']) }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">Unique sessions</p>
|
||||
<p class="mt-3 text-3xl font-bold text-white">{{ number_format($overview['unique_sessions']) }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">Distinct tags</p>
|
||||
<p class="mt-3 text-3xl font-bold text-white">{{ number_format($overview['distinct_tags']) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">Daily Click Trend</h2>
|
||||
<p class="mt-1 text-sm text-neutral-500">Daily rollups for tuning trending and recommendation decisions.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
@forelse($dailyClicks as $row)
|
||||
<div class="rounded-lg border border-neutral-800 bg-neutral-950/70 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-neutral-500">{{ $row['date'] }}</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($row['clicks']) }}</p>
|
||||
<p class="mt-1 text-xs text-neutral-500">clicks</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-neutral-500">No aggregated rows available for the selected range yet.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<section class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<h2 class="text-lg font-semibold text-white">Top Surfaces</h2>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-neutral-800 text-left text-xs uppercase tracking-[0.18em] text-neutral-500">
|
||||
<th class="pb-3 pr-4">Surface</th>
|
||||
<th class="pb-3 pr-4">Clicks</th>
|
||||
<th class="pb-3 pr-4">Users</th>
|
||||
<th class="pb-3 pr-4">Sessions</th>
|
||||
<th class="pb-3">Avg pos.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($bySurface as $row)
|
||||
<tr class="border-b border-neutral-800/70 text-neutral-200">
|
||||
<td class="py-3 pr-4 font-medium text-white">{{ $row['surface'] }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['clicks']) }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['unique_users']) }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['unique_sessions']) }}</td>
|
||||
<td class="py-3">{{ $row['avg_position'] > 0 ? number_format($row['avg_position'], 2) : 'n/a' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="py-4 text-neutral-500">No surface data in this range.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<h2 class="text-lg font-semibold text-white">Top Query Terms</h2>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-neutral-800 text-left text-xs uppercase tracking-[0.18em] text-neutral-500">
|
||||
<th class="pb-3 pr-4">Query</th>
|
||||
<th class="pb-3 pr-4">Clicks</th>
|
||||
<th class="pb-3 pr-4">Sessions</th>
|
||||
<th class="pb-3">Resolved tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($topQueries as $row)
|
||||
<tr class="border-b border-neutral-800/70 text-neutral-200">
|
||||
<td class="py-3 pr-4 font-medium text-white">{{ $row['query'] }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['clicks']) }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['unique_sessions']) }}</td>
|
||||
<td class="py-3">{{ number_format($row['resolved_tags']) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="py-4 text-neutral-500">No query data in this range.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<section class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<h2 class="text-lg font-semibold text-white">Top Tags</h2>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-neutral-800 text-left text-xs uppercase tracking-[0.18em] text-neutral-500">
|
||||
<th class="pb-3 pr-4">Tag</th>
|
||||
<th class="pb-3 pr-4">Clicks</th>
|
||||
<th class="pb-3 pr-4">Recommendation</th>
|
||||
<th class="pb-3 pr-4">Search</th>
|
||||
<th class="pb-3">Sessions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($topTags as $row)
|
||||
<tr class="border-b border-neutral-800/70 text-neutral-200">
|
||||
<td class="py-3 pr-4 font-medium text-white">#{{ $row['tag_slug'] }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['clicks']) }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['recommendation_clicks']) }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['search_clicks']) }}</td>
|
||||
<td class="py-3">{{ number_format($row['unique_sessions']) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="py-4 text-neutral-500">No tag click data in this range.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-neutral-800 bg-neutral-900 p-5">
|
||||
<h2 class="text-lg font-semibold text-white">Top Tag Transitions</h2>
|
||||
<p class="mt-1 text-sm text-neutral-500">Most-clicked source tag to target tag paths from related-tag surfaces.</p>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-neutral-800 text-left text-xs uppercase tracking-[0.18em] text-neutral-500">
|
||||
<th class="pb-3 pr-4">Source</th>
|
||||
<th class="pb-3 pr-4">Target</th>
|
||||
<th class="pb-3 pr-4">Clicks</th>
|
||||
<th class="pb-3 pr-4">Sessions</th>
|
||||
<th class="pb-3">Avg pos.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($topTransitions as $row)
|
||||
<tr class="border-b border-neutral-800/70 text-neutral-200">
|
||||
<td class="py-3 pr-4 font-medium text-white">#{{ $row['source_tag_slug'] }}</td>
|
||||
<td class="py-3 pr-4 font-medium text-white">#{{ $row['tag_slug'] }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['clicks']) }}</td>
|
||||
<td class="py-3 pr-4">{{ number_format($row['unique_sessions']) }}</td>
|
||||
<td class="py-3">{{ $row['avg_position'] > 0 ? number_format($row['avg_position'], 2) : 'n/a' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="py-4 text-neutral-500">No transition data in this range.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user