mad-lawsuit/resources/js/Pages/Admin/Dashboard.vue
friday-bot 04bd71aaaa Add document access statistics to admin dashboard
Records every successful document download and surfaces the counts in the
admin area, so the docket owner can see which filings are being pulled and
how often.

- document_accesses table + DocumentAccess model
- DocumentController@download records each successful download with IP and
  user agent
- documents:import-access-log backfills history from an archived nginx log
- Dashboard gains a 60-day downloads card and a most-requested list
- New Document Access page with a date-range filter and per-day trend

Client IPs were only captured from 2026-07-30, when the reverse proxy chain
started forwarding X-Forwarded-For correctly. Rows imported from the old
nginx log therefore have no IP, and the UI labels those periods as download
counts rather than unique visitors instead of showing a misleading number.
2026-07-30 13:14:01 -06:00

262 lines
13 KiB
Vue

<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import { ref } from 'vue';
interface DocketEntry {
id: number;
date: string;
title: string;
summary: string;
documents_count: number;
}
interface TopDocument {
id: number;
title: string;
downloads: number;
}
interface Props {
total_entries: number;
total_documents: number;
total_subscribers: number;
recent_entries: DocketEntry[];
downloads_60d: number;
top_documents_60d: TopDocument[];
}
const props = defineProps<Props>();
const logout = () => {
router.post(route('admin.logout'));
};
</script>
<template>
<div class="min-h-screen bg-gray-100">
<Head title="Admin Dashboard" />
<!-- Header -->
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900">
Admin Dashboard
</h1>
<p class="text-sm text-gray-600 mt-1">
Eliza Kragh v. Montana Association of the Deaf
</p>
</div>
<div class="flex gap-4">
<Link
href="/"
class="px-4 py-2 text-sm text-gray-700 hover:text-gray-900 transition-colors"
>
View Public Site
</Link>
<button
@click="logout"
class="px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors"
>
Logout
</button>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Total Entries -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Total Entries</p>
<p class="text-3xl font-bold text-gray-900 mt-2">
{{ props.total_entries }}
</p>
</div>
<div class="p-3 bg-blue-100 rounded-full">
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
</div>
<!-- Total Documents -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Total Documents</p>
<p class="text-3xl font-bold text-gray-900 mt-2">
{{ props.total_documents }}
</p>
</div>
<div class="p-3 bg-green-100 rounded-full">
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
</div>
</div>
<!-- Total Subscribers -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Active Subscribers</p>
<p class="text-3xl font-bold text-gray-900 mt-2">
{{ props.total_subscribers }}
</p>
</div>
<div class="p-3 bg-purple-100 rounded-full">
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
</div>
</div>
<!-- Document Downloads (60 days) -->
<Link
:href="route('admin.document-access.index')"
class="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow"
>
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Downloads (60 days)</p>
<p class="text-3xl font-bold text-gray-900 mt-2">
{{ props.downloads_60d }}
</p>
<p class="text-xs text-gray-500 mt-1">Excludes crawlers</p>
</div>
<div class="p-3 bg-amber-100 rounded-full">
<svg class="w-8 h-8 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</div>
</div>
</Link>
</div>
<!-- Most Requested Documents -->
<div class="bg-white rounded-lg shadow mb-8">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<div>
<h2 class="text-xl font-bold text-gray-900">Most Requested Documents</h2>
<p class="text-sm text-gray-600 mt-1">Last 60 days</p>
</div>
<Link
:href="route('admin.document-access.index')"
class="px-4 py-2 text-sm text-blue-600 hover:text-blue-800 transition-colors"
>
View all &amp; change dates &rarr;
</Link>
</div>
<div class="divide-y divide-gray-200">
<div
v-for="doc in props.top_documents_60d"
:key="doc.id"
class="px-6 py-4 flex justify-between items-center hover:bg-gray-50 transition-colors"
>
<span class="text-sm font-medium text-gray-900">{{ doc.title }}</span>
<span class="px-3 py-1 text-xs font-medium text-amber-700 bg-amber-100 rounded-full whitespace-nowrap ml-4">
{{ doc.downloads }} download{{ doc.downloads !== 1 ? 's' : '' }}
</span>
</div>
<div v-if="props.top_documents_60d.length === 0" class="px-6 py-8 text-center text-gray-500">
No document downloads recorded in the last 60 days.
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-white rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-bold text-gray-900 mb-4">Quick Actions</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<Link
:href="route('admin.docket-entries.create')"
class="flex items-center justify-center px-6 py-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Create New Entry
</Link>
<Link
:href="route('admin.docket-entries.index')"
class="flex items-center justify-center px-6 py-4 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
Manage Entries
</Link>
<Link
:href="route('admin.subscribers.index')"
class="flex items-center justify-center px-6 py-4 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
View Subscribers
</Link>
</div>
</div>
<!-- Recent Entries -->
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">Recent Entries</h2>
</div>
<div class="divide-y divide-gray-200">
<div
v-for="entry in props.recent_entries"
:key="entry.id"
class="px-6 py-4 hover:bg-gray-50 transition-colors"
>
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<span class="text-sm font-medium text-gray-500">
{{ entry.date }}
</span>
<span class="px-2 py-1 text-xs font-medium text-blue-700 bg-blue-100 rounded-full">
{{ entry.documents_count }} document{{ entry.documents_count !== 1 ? 's' : '' }}
</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-1">
{{ entry.title }}
</h3>
<p class="text-sm text-gray-600 line-clamp-2">
{{ entry.summary }}
</p>
</div>
<div class="flex gap-2 ml-4">
<Link
:href="route('admin.docket-entries.edit', entry.id)"
class="px-3 py-1 text-sm text-blue-600 hover:text-blue-800 transition-colors"
>
Edit
</Link>
<Link
:href="route('admin.docket-entries.show', entry.id)"
class="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 transition-colors"
>
View
</Link>
</div>
</div>
</div>
<div v-if="props.recent_entries.length === 0" class="px-6 py-8 text-center text-gray-500">
No entries yet. Create your first entry to get started.
</div>
</div>
</div>
</main>
</div>
</template>