Files
infohliadka/pages/admin/reports.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

244 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import AdminLayout from '../../components/AdminLayout'
import { DocumentTextIcon, ExclamationTriangleIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline'
interface Report {
id: number
source_url: string
source_domain: string
reporter_email?: string
reporter_name?: string
category_suggestions: string[]
description: string
priority: string
status: string
created_at: string
}
const ReportsManagement: NextPage = () => {
const [reports, setReports] = useState<Report[]>([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState('pending')
const fetchReports = useCallback(async () => {
try {
const response = await fetch(`/api/admin/reports?status=${filter}`)
if (response.ok) {
const data = await response.json()
setReports(data)
}
} catch (error) {
console.error('Failed to fetch reports:', error)
} finally {
setLoading(false)
}
}, [filter])
useEffect(() => {
fetchReports()
}, [fetchReports])
const handleStatusChange = async (id: number, newStatus: string) => {
try {
const response = await fetch(`/api/admin/reports/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
})
if (response.ok) {
fetchReports()
}
} catch (error) {
console.error('Failed to update report status:', error)
}
}
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'bg-red-100 text-red-800'
case 'medium': return 'bg-yellow-100 text-yellow-800'
case 'low': return 'bg-green-100 text-green-800'
default: return 'bg-gray-100 text-gray-800'
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'pending': return 'bg-yellow-100 text-yellow-800'
case 'approved': return 'bg-green-100 text-green-800'
case 'rejected': return 'bg-red-100 text-red-800'
case 'processing': return 'bg-blue-100 text-blue-800'
default: return 'bg-gray-100 text-gray-800'
}
}
return (
<>
<Head>
<title>Hlásenia - Hliadka.sk Admin</title>
</Head>
<AdminLayout title="Správa hlásení">
<div className="px-4 py-6 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<p className="text-sm text-gray-700">
Prehľad a spracovanie hlásení od používateľov
</p>
</div>
</div>
<div className="mt-6">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{[
{ key: 'pending', label: 'Čakajúce', count: reports.filter(r => r.status === 'pending').length },
{ key: 'processing', label: 'Spracovávané', count: reports.filter(r => r.status === 'processing').length },
{ key: 'approved', label: 'Schválené', count: reports.filter(r => r.status === 'approved').length },
{ key: 'rejected', label: 'Zamietnuté', count: reports.filter(r => r.status === 'rejected').length },
].map((tab) => (
<button
key={tab.key}
onClick={() => setFilter(tab.key)}
className={`${
filter === tab.key
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
} flex whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm`}
>
{tab.label}
{tab.count > 0 && (
<span className={`${
filter === tab.key ? 'bg-primary-100 text-primary-600' : 'bg-gray-100 text-gray-900'
} ml-3 py-0.5 px-2.5 rounded-full text-xs font-medium`}>
{tab.count}
</span>
)}
</button>
))}
</nav>
</div>
</div>
<div className="mt-8">
{loading ? (
<div className="card p-6">
<div className="flex items-center justify-center">
<div className="text-gray-500">Načítavanie hlásení...</div>
</div>
</div>
) : reports.length === 0 ? (
<div className="card p-6 text-center">
<DocumentTextIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-semibold text-gray-900">Žiadne hlásenia</h3>
<p className="mt-1 text-sm text-gray-500">
{filter === 'pending' ? 'Momentálne nie sú žiadne čakajúce hlásenia.' : `Žiadne ${filter} hlásenia.`}
</p>
</div>
) : (
<div className="space-y-4">
{reports.map((report) => (
<div key={report.id} className="card p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3">
<DocumentTextIcon className="h-5 w-5 text-gray-400" />
<h3 className="text-lg font-medium text-gray-900">
{report.source_domain}
</h3>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPriorityColor(report.priority)}`}>
{report.priority === 'high' ? 'Vysoká' : report.priority === 'medium' ? 'Stredná' : 'Nízka'} priorita
</span>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(report.status)}`}>
{report.status === 'pending' ? 'Čaká' :
report.status === 'approved' ? 'Schválené' :
report.status === 'rejected' ? 'Zamietnuté' : 'Spracováva sa'}
</span>
</div>
<div className="mt-2">
<p className="text-sm text-gray-600">{report.description}</p>
<div className="mt-2">
<span className="text-xs text-gray-500">URL: </span>
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">{report.source_url}</code>
</div>
{report.category_suggestions.length > 0 && (
<div className="mt-2">
<span className="text-xs text-gray-500">Navrhované kategórie: </span>
<div className="flex flex-wrap gap-1 mt-1">
{report.category_suggestions.map((category, idx) => (
<span key={idx} className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800">
{category}
</span>
))}
</div>
</div>
)}
</div>
<div className="mt-4 flex items-center text-sm text-gray-500">
<span>Nahlásené: {new Date(report.created_at).toLocaleString('sk-SK')}</span>
{report.reporter_email && (
<span className="ml-4">Reporter: {report.reporter_email}</span>
)}
</div>
</div>
<div className="flex space-x-2 ml-4">
{report.status === 'pending' && (
<>
<button
onClick={() => handleStatusChange(report.id, 'processing')}
className="btn-secondary text-xs px-3 py-1"
>
Spracovať
</button>
<button
onClick={() => handleStatusChange(report.id, 'approved')}
className="text-xs px-3 py-1 bg-green-600 hover:bg-green-700 text-white font-medium rounded-md transition-colors duration-200 flex items-center justify-center"
>
<CheckCircleIcon className="h-3 w-3 mr-1" />
Schváliť
</button>
<button
onClick={() => handleStatusChange(report.id, 'rejected')}
className="text-xs px-3 py-1 bg-red-600 hover:bg-red-700 text-white font-medium rounded-md transition-colors duration-200 flex items-center justify-center"
>
<XCircleIcon className="h-3 w-3 mr-1" />
Zamietnuť
</button>
</>
)}
{report.status === 'processing' && (
<>
<button
onClick={() => handleStatusChange(report.id, 'approved')}
className="text-xs px-3 py-1 bg-green-600 hover:bg-green-700 text-white font-medium rounded-md transition-colors duration-200 flex items-center justify-center"
>
<CheckCircleIcon className="h-3 w-3 mr-1" />
Schváliť
</button>
<button
onClick={() => handleStatusChange(report.id, 'rejected')}
className="text-xs px-3 py-1 bg-red-600 hover:bg-red-700 text-white font-medium rounded-md transition-colors duration-200 flex items-center justify-center"
>
<XCircleIcon className="h-3 w-3 mr-1" />
Zamietnuť
</button>
</>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</AdminLayout>
</>
)
}
export default ReportsManagement