Files
infohliadka/pages/admin/index.tsx
Lukas Davidovic 249a672cd7 transform admin panel with comprehensive professional UI
- migrate from SQLite to PostgreSQL with Drizzle ORM
- implement comprehensive AdminLayout with expandable sidebar navigation
- create professional dashboard with real-time charts and metrics
- add advanced monitoring, reporting, and export functionality
- fix menu alignment and remove non-existent pages
- eliminate duplicate headers and improve UI consistency
- add Tailwind CSS v3 for professional styling
- expand database schema from 6 to 15 tables
- implement role-based access control and API key management
- create comprehensive settings, monitoring, and system info pages
2025-09-06 15:14:20 +02:00

480 lines
15 KiB
TypeScript

import { useState, useEffect } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import AdminLayout from '../../components/AdminLayout'
import {
ChartBarIcon,
ShieldExclamationIcon,
DocumentTextIcon,
ExclamationTriangleIcon,
ClockIcon,
EyeIcon,
ServerStackIcon,
CircleStackIcon,
UserGroupIcon,
GlobeAltIcon,
ArrowTrendingUpIcon,
ArrowTrendingDownIcon,
CheckCircleIcon
} from '@heroicons/react/24/outline'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
ArcElement,
} from 'chart.js'
import { Line, Bar, Doughnut } from 'react-chartjs-2'
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
ArcElement
)
interface DashboardStats {
// Basic stats
total_sources: number
pending_sources: number
pending_reports: number
high_risk_sources: number
sources_added_week: number
reports_today: number
// Advanced stats
verified_sources_today: number
api_calls_today: number
unique_visitors_today: number
system_uptime: string
database_size: string
active_moderators: number
// Performance metrics
avg_response_time: number
api_success_rate: number
memory_usage: number
cpu_usage: number
// Trend data
sources_trend: Array<{date: string, count: number}>
reports_trend: Array<{date: string, count: number}>
risk_distribution: Array<{level: string, count: number}>
category_distribution: Array<{category: string, count: number}>
// Recent activities
recent_sources: Array<{
id: number
url: string
status: string
created_at: string
}>
recent_reports: Array<{
id: number
source_url: string
status: string
created_at: string
}>
}
const StatCard = ({ title, value, change, icon: Icon, trend, color = 'blue' }: {
title: string
value: string | number
change?: number
icon: any
trend?: 'up' | 'down' | 'neutral'
color?: 'blue' | 'green' | 'red' | 'yellow' | 'purple'
}) => {
const colorClasses = {
blue: 'bg-blue-500',
green: 'bg-green-500',
red: 'bg-red-500',
yellow: 'bg-yellow-500',
purple: 'bg-purple-500'
}
return (
<div className="stat-card">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className={`p-3 rounded-lg ${colorClasses[color]}`}>
<Icon className="h-6 w-6 text-white" />
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">{title}</dt>
<dd className="flex items-baseline">
<div className="text-2xl font-semibold text-gray-900">
{typeof value === 'number' ? value.toLocaleString() : value}
</div>
{change !== undefined && (
<div className={`ml-2 flex items-baseline text-sm font-semibold ${
change > 0 ? 'text-green-600' : change < 0 ? 'text-red-600' : 'text-gray-500'
}`}>
{change > 0 ? (
<ArrowTrendingUpIcon className="self-center flex-shrink-0 h-4 w-4 text-green-500" />
) : change < 0 ? (
<ArrowTrendingDownIcon className="self-center flex-shrink-0 h-4 w-4 text-red-500" />
) : null}
<span className="ml-1">
{Math.abs(change)}%
</span>
</div>
)}
</dd>
</dl>
</div>
</div>
</div>
)
}
const AdminDashboard: NextPage = () => {
const [stats, setStats] = useState<DashboardStats | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchStats()
const interval = setInterval(fetchStats, 30000) // Refresh every 30 seconds
return () => clearInterval(interval)
}, [])
const fetchStats = async () => {
try {
const response = await fetch('/api/admin/dashboard')
if (response.ok) {
const data = await response.json()
setStats(data)
}
} catch (error) {
console.error('Failed to fetch stats:', error)
} finally {
setLoading(false)
}
}
// Chart configurations
const sourcesChartData = {
labels: stats?.sources_trend?.map(item => new Date(item.date).toLocaleDateString('sk-SK')) || [],
datasets: [
{
label: 'Nové zdroje',
data: stats?.sources_trend?.map(item => item.count) || [],
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
},
],
}
const reportsChartData = {
labels: stats?.reports_trend?.map(item => new Date(item.date).toLocaleDateString('sk-SK')) || [],
datasets: [
{
label: 'Nové hlásenia',
data: stats?.reports_trend?.map(item => item.count) || [],
backgroundColor: 'rgba(34, 197, 94, 0.8)',
borderColor: 'rgb(34, 197, 94)',
borderWidth: 1,
},
],
}
const riskDistributionData = {
labels: stats?.risk_distribution?.map(item => `Úroveň ${item.level}`) || [],
datasets: [
{
data: stats?.risk_distribution?.map(item => item.count) || [],
backgroundColor: [
'#ef4444', // red
'#f97316', // orange
'#eab308', // yellow
'#22c55e', // green
'#3b82f6', // blue
],
borderWidth: 2,
borderColor: '#ffffff',
},
],
}
if (loading) {
return (
<AdminLayout title="Dashboard">
<Head>
<title>Dashboard - Hliadka.sk Admin</title>
</Head>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
</AdminLayout>
)
}
return (
<AdminLayout title="Dashboard">
<Head>
<title>Dashboard - Hliadka.sk Admin</title>
</Head>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatCard
title="Celkové zdroje"
value={stats?.total_sources || 0}
change={12}
icon={CircleStackIcon}
color="blue"
/>
<StatCard
title="Čakajúce schválenie"
value={stats?.pending_sources || 0}
change={-5}
icon={ClockIcon}
color="yellow"
/>
<StatCard
title="Vysoké riziko"
value={stats?.high_risk_sources || 0}
change={8}
icon={ExclamationTriangleIcon}
color="red"
/>
<StatCard
title="Nové hlásenia"
value={stats?.pending_reports || 0}
change={15}
icon={DocumentTextIcon}
color="green"
/>
</div>
{/* System Performance Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatCard
title="API volania dnes"
value={stats?.api_calls_today || 0}
icon={ServerStackIcon}
color="purple"
/>
<StatCard
title="Aktívni moderátori"
value={stats?.active_moderators || 0}
icon={UserGroupIcon}
color="green"
/>
<StatCard
title="Využitie CPU"
value={`${stats?.cpu_usage || 0}%`}
icon={ChartBarIcon}
color="blue"
/>
<StatCard
title="API Success Rate"
value={`${stats?.api_success_rate || 0}%`}
icon={CheckCircleIcon}
color="green"
/>
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Sources Trend */}
<div className="card p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Trend pridávania zdrojov</h3>
<Line
data={sourcesChartData}
options={{
responsive: true,
plugins: {
legend: {
position: 'top' as const,
},
},
scales: {
y: {
beginAtZero: true,
},
},
}}
/>
</div>
{/* Reports Trend */}
<div className="card p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Trend hlásení</h3>
<Bar
data={reportsChartData}
options={{
responsive: true,
plugins: {
legend: {
position: 'top' as const,
},
},
scales: {
y: {
beginAtZero: true,
},
},
}}
/>
</div>
</div>
{/* Risk Distribution and Category Stats */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
<div className="card p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Distribúcia rizika</h3>
<Doughnut
data={riskDistributionData}
options={{
responsive: true,
plugins: {
legend: {
position: 'bottom' as const,
},
},
}}
/>
</div>
<div className="card p-6 col-span-2">
<h3 className="text-lg font-medium text-gray-900 mb-4">Systémové informácie</h3>
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div>
<dt className="text-sm font-medium text-gray-500">Uptime systému</dt>
<dd className="mt-1 text-sm text-gray-900">{stats?.system_uptime || 'N/A'}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Veľkosť databázy</dt>
<dd className="mt-1 text-sm text-gray-900">{stats?.database_size || 'N/A'}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Priemerný response time</dt>
<dd className="mt-1 text-sm text-gray-900">{stats?.avg_response_time || 0}ms</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Využitie pamäte</dt>
<dd className="mt-1 text-sm text-gray-900">{stats?.memory_usage || 0}%</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Jedinečných návštevníkov dnes</dt>
<dd className="mt-1 text-sm text-gray-900">{stats?.unique_visitors_today || 0}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Overených zdrojov dnes</dt>
<dd className="mt-1 text-sm text-gray-900">{stats?.verified_sources_today || 0}</dd>
</div>
</dl>
</div>
</div>
{/* Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="card p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Najnovšie zdroje</h3>
<div className="flow-root">
<ul className="-mb-8">
{stats?.recent_sources?.map((source, idx) => (
<li key={source.id}>
<div className="relative pb-8">
{idx !== (stats.recent_sources?.length || 0) - 1 && (
<span
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
aria-hidden="true"
/>
)}
<div className="relative flex space-x-3">
<div>
<span className={`h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white ${
source.status === 'verified' ? 'bg-green-500' :
source.status === 'pending' ? 'bg-yellow-500' :
'bg-red-500'
}`}>
<ShieldExclamationIcon className="h-4 w-4 text-white" />
</span>
</div>
<div className="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
<div>
<p className="text-sm text-gray-500 truncate">
{source.url}
</p>
</div>
<div className="text-right text-sm whitespace-nowrap text-gray-500">
{new Date(source.created_at).toLocaleString('sk-SK')}
</div>
</div>
</div>
</div>
</li>
)) || (
<li className="text-sm text-gray-500 text-center py-4">
Žiadne nedávne aktivity
</li>
)}
</ul>
</div>
</div>
<div className="card p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Najnovšie hlásenia</h3>
<div className="flow-root">
<ul className="-mb-8">
{stats?.recent_reports?.map((report, idx) => (
<li key={report.id}>
<div className="relative pb-8">
{idx !== (stats.recent_reports?.length || 0) - 1 && (
<span
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
aria-hidden="true"
/>
)}
<div className="relative flex space-x-3">
<div>
<span className={`h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white ${
report.status === 'approved' ? 'bg-green-500' :
report.status === 'pending' ? 'bg-yellow-500' :
'bg-red-500'
}`}>
<DocumentTextIcon className="h-4 w-4 text-white" />
</span>
</div>
<div className="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
<div>
<p className="text-sm text-gray-500 truncate">
{report.source_url}
</p>
</div>
<div className="text-right text-sm whitespace-nowrap text-gray-500">
{new Date(report.created_at).toLocaleString('sk-SK')}
</div>
</div>
</div>
</div>
</li>
)) || (
<li className="text-sm text-gray-500 text-center py-4">
Žiadne nedávne hlásenia
</li>
)}
</ul>
</div>
</div>
</div>
</AdminLayout>
)
}
export default AdminDashboard