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
This commit is contained in:
@@ -1,15 +1,143 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
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 = () => {
|
||||
@@ -18,6 +146,8 @@ const AdminDashboard: NextPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats()
|
||||
const interval = setInterval(fetchStats, 30000) // Refresh every 30 seconds
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const fetchStats = async () => {
|
||||
@@ -34,85 +164,316 @@ const AdminDashboard: NextPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div>
|
||||
<AdminLayout title="Dashboard">
|
||||
<Head>
|
||||
<title>Admin Panel - Infohliadka</title>
|
||||
<title>Dashboard - Hliadka.sk Admin</title>
|
||||
</Head>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
<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 (
|
||||
<div>
|
||||
<AdminLayout title="Dashboard">
|
||||
<Head>
|
||||
<title>Admin Panel - Infohliadka</title>
|
||||
<title>Dashboard - Hliadka.sk Admin</title>
|
||||
</Head>
|
||||
|
||||
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
|
||||
<h1>Admin Dashboard</h1>
|
||||
|
||||
{stats && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '20px', marginTop: '30px' }}>
|
||||
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
|
||||
<h3>Celkové zdroje</h3>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>{stats.total_sources}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
|
||||
<h3>Čakajúce schválenie</h3>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#f59e0b' }}>{stats.pending_sources}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
|
||||
<h3>Vysoké riziko</h3>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#ef4444' }}>{stats.high_risk_sources}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
|
||||
<h3>Nové hlásenia</h3>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#3b82f6' }}>{stats.pending_reports}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
|
||||
<h3>Pridané tento týždeň</h3>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>{stats.sources_added_week}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
|
||||
<h3>Hlásenia dnes</h3>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>{stats.reports_today}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 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>
|
||||
|
||||
<div style={{ marginTop: '40px' }}>
|
||||
<h2>Rýchle akcie</h2>
|
||||
<div style={{ display: 'flex', gap: '15px', marginTop: '20px' }}>
|
||||
<Link href="/admin/sources" style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
Správa zdrojov
|
||||
</Link>
|
||||
<Link href="/admin/reports" style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#10b981',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
Hlásenia
|
||||
</Link>
|
||||
{/* 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>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user