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,7 +1,8 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import type { NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import AdminLayout from '../../components/AdminLayout'
|
||||
import { DocumentTextIcon, ExclamationTriangleIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
interface Report {
|
||||
id: number
|
||||
@@ -39,151 +40,204 @@ const ReportsManagement: NextPage = () => {
|
||||
fetchReports()
|
||||
}, [fetchReports])
|
||||
|
||||
const updateReport = async (id: number, status: string, notes?: string) => {
|
||||
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,
|
||||
admin_notes: notes,
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
fetchReports()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update report:', error)
|
||||
console.error('Failed to update report status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'urgent': return '#dc2626'
|
||||
case 'high': return '#ea580c'
|
||||
case 'medium': return '#d97706'
|
||||
default: return '#6b7280'
|
||||
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 (
|
||||
<div>
|
||||
<>
|
||||
<Head>
|
||||
<title>Správa hlásení - Infohliadka</title>
|
||||
<title>Hlásenia - Hliadka.sk Admin</title>
|
||||
</Head>
|
||||
|
||||
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
|
||||
<h1>Správa hlásení</h1>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label>Filter: </label>
|
||||
<select
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
style={{ padding: '8px', marginLeft: '10px' }}
|
||||
>
|
||||
<option value="pending">Čakajúce</option>
|
||||
<option value="in_review">V spracovaní</option>
|
||||
<option value="approved">Schválené</option>
|
||||
<option value="rejected">Zamietnuté</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#f3f4f6' }}>
|
||||
<th style={{ padding: '12px', textAlign: 'left', border: '1px solid #d1d5db' }}>Doména</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', border: '1px solid #d1d5db' }}>Kategórie</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', border: '1px solid #d1d5db' }}>Priorita</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', border: '1px solid #d1d5db' }}>Status</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', border: '1px solid #d1d5db' }}>Dátum</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', border: '1px solid #d1d5db' }}>Akcie</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reports.map((report) => (
|
||||
<tr key={report.id}>
|
||||
<td style={{ padding: '12px', border: '1px solid #d1d5db' }}>
|
||||
<a href={report.source_url} target="_blank" rel="noopener noreferrer">
|
||||
{report.source_domain}
|
||||
</a>
|
||||
<div style={{ fontSize: '12px', color: '#6b7280' }}>
|
||||
{report.reporter_name || 'Anonymous'}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '12px', border: '1px solid #d1d5db' }}>
|
||||
{report.category_suggestions.join(', ')}
|
||||
</td>
|
||||
<td style={{ padding: '12px', border: '1px solid #d1d5db' }}>
|
||||
<span style={{ color: getPriorityColor(report.priority), fontWeight: 'bold' }}>
|
||||
{report.priority}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px', border: '1px solid #d1d5db' }}>{report.status}</td>
|
||||
<td style={{ padding: '12px', border: '1px solid #d1d5db' }}>
|
||||
{new Date(report.created_at).toLocaleDateString('sk-SK')}
|
||||
</td>
|
||||
<td style={{ padding: '12px', border: '1px solid #d1d5db' }}>
|
||||
{report.status === 'pending' && (
|
||||
<div style={{ display: 'flex', gap: '5px', flexDirection: 'column' }}>
|
||||
<button
|
||||
onClick={() => updateReport(report.id, 'approved')}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
backgroundColor: '#10b981',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Schváliť
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateReport(report.id, 'rejected', 'Insufficient evidence')}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
backgroundColor: '#ef4444',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Zamietnuť
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<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 style={{ marginTop: '30px' }}>
|
||||
<Link href="/admin" style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#6b7280',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
← Späť na dashboard
|
||||
</Link>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user