- 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
396 lines
16 KiB
TypeScript
396 lines
16 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import type { NextPage } from 'next'
|
|
import Head from 'next/head'
|
|
import AdminLayout from '../../components/AdminLayout'
|
|
import {
|
|
DocumentArrowDownIcon,
|
|
TableCellsIcon,
|
|
ChartBarIcon,
|
|
CalendarIcon,
|
|
FunnelIcon,
|
|
Cog6ToothIcon,
|
|
ClockIcon,
|
|
CheckCircleIcon,
|
|
ExclamationTriangleIcon
|
|
} from '@heroicons/react/24/outline'
|
|
|
|
interface ExportJob {
|
|
id: string
|
|
name: string
|
|
type: 'sources' | 'reports' | 'analytics' | 'users' | 'audit_logs'
|
|
format: 'csv' | 'xlsx' | 'json' | 'pdf'
|
|
status: 'pending' | 'processing' | 'completed' | 'failed'
|
|
created_at: string
|
|
completed_at?: string
|
|
download_url?: string
|
|
file_size?: string
|
|
records_count?: number
|
|
}
|
|
|
|
const ExportPage: NextPage = () => {
|
|
const [exportJobs, setExportJobs] = useState<ExportJob[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [selectedType, setSelectedType] = useState<string>('sources')
|
|
const [selectedFormat, setSelectedFormat] = useState<string>('csv')
|
|
const [dateRange, setDateRange] = useState({
|
|
from: '',
|
|
to: ''
|
|
})
|
|
const [filters, setFilters] = useState({
|
|
status: '',
|
|
riskLevel: '',
|
|
category: ''
|
|
})
|
|
|
|
useEffect(() => {
|
|
fetchExportJobs()
|
|
}, [])
|
|
|
|
const fetchExportJobs = async () => {
|
|
try {
|
|
const response = await fetch('/api/admin/export/jobs')
|
|
if (response.ok) {
|
|
const jobs = await response.json()
|
|
setExportJobs(jobs)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch export jobs:', error)
|
|
}
|
|
}
|
|
|
|
const createExport = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const response = await fetch('/api/admin/export', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: selectedType,
|
|
format: selectedFormat,
|
|
dateRange,
|
|
filters
|
|
})
|
|
})
|
|
|
|
if (response.ok) {
|
|
const newJob = await response.json()
|
|
setExportJobs(prev => [newJob, ...prev])
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to create export:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return <CheckCircleIcon className="h-5 w-5 text-green-500" />
|
|
case 'failed':
|
|
return <ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
case 'processing':
|
|
return <div className="h-5 w-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
|
default:
|
|
return <ClockIcon className="h-5 w-5 text-yellow-500" />
|
|
}
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return 'bg-green-100 text-green-800'
|
|
case 'failed':
|
|
return 'bg-red-100 text-red-800'
|
|
case 'processing':
|
|
return 'bg-blue-100 text-blue-800'
|
|
default:
|
|
return 'bg-yellow-100 text-yellow-800'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AdminLayout title="Export dát">
|
|
<Head>
|
|
<title>Export dát - Hliadka.sk Admin</title>
|
|
</Head>
|
|
|
|
<div className="space-y-8">
|
|
{/* Export Configuration */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<DocumentArrowDownIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">Nový export</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Export Type Selection */}
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Typ dát</h3>
|
|
<div className="space-y-3">
|
|
{[
|
|
{ value: 'sources', label: 'Zdroje', description: 'Všetky monitorované zdroje s metadátami', icon: TableCellsIcon },
|
|
{ value: 'reports', label: 'Hlásenia', description: 'Používateľské hlásenia a ich stav', icon: DocumentArrowDownIcon },
|
|
{ value: 'analytics', label: 'Analytika', description: 'Štatistické údaje a metriky', icon: ChartBarIcon },
|
|
{ value: 'users', label: 'Používatelia', description: 'Administrátori a moderátori', icon: TableCellsIcon },
|
|
{ value: 'audit_logs', label: 'Audit log', description: 'Záznamy o aktivitách v systéme', icon: ClockIcon }
|
|
].map((type) => (
|
|
<label key={type.value} className="flex items-center p-4 border rounded-lg cursor-pointer hover:bg-gray-50">
|
|
<input
|
|
type="radio"
|
|
name="exportType"
|
|
value={type.value}
|
|
checked={selectedType === type.value}
|
|
onChange={(e) => setSelectedType(e.target.value)}
|
|
className="h-4 w-4 text-primary-600"
|
|
/>
|
|
<type.icon className="h-5 w-5 text-gray-400 ml-3 mr-3" />
|
|
<div>
|
|
<div className="font-medium text-gray-900">{type.label}</div>
|
|
<div className="text-sm text-gray-500">{type.description}</div>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Export Configuration */}
|
|
<div className="space-y-6">
|
|
{/* Format Selection */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Formát súboru</label>
|
|
<select
|
|
value={selectedFormat}
|
|
onChange={(e) => setSelectedFormat(e.target.value)}
|
|
className="input"
|
|
>
|
|
<option value="csv">CSV (Comma Separated Values)</option>
|
|
<option value="xlsx">XLSX (Excel)</option>
|
|
<option value="json">JSON</option>
|
|
<option value="pdf">PDF Report</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Date Range */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Časové obdobie</label>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">Od</label>
|
|
<input
|
|
type="date"
|
|
value={dateRange.from}
|
|
onChange={(e) => setDateRange(prev => ({ ...prev, from: e.target.value }))}
|
|
className="input"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">Do</label>
|
|
<input
|
|
type="date"
|
|
value={dateRange.to}
|
|
onChange={(e) => setDateRange(prev => ({ ...prev, to: e.target.value }))}
|
|
className="input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
{selectedType === 'sources' && (
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-medium text-gray-700">Filtre</h4>
|
|
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">Status</label>
|
|
<select
|
|
value={filters.status}
|
|
onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value }))}
|
|
className="input"
|
|
>
|
|
<option value="">Všetky</option>
|
|
<option value="pending">Čakajúce</option>
|
|
<option value="verified">Overené</option>
|
|
<option value="rejected">Zamietnuté</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">Úroveň rizika</label>
|
|
<select
|
|
value={filters.riskLevel}
|
|
onChange={(e) => setFilters(prev => ({ ...prev, riskLevel: e.target.value }))}
|
|
className="input"
|
|
>
|
|
<option value="">Všetky</option>
|
|
<option value="1">1 - Nízke</option>
|
|
<option value="2">2 - Mierne</option>
|
|
<option value="3">3 - Stredné</option>
|
|
<option value="4">4 - Vysoké</option>
|
|
<option value="5">5 - Kritické</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Export Button */}
|
|
<button
|
|
onClick={createExport}
|
|
disabled={loading}
|
|
className="btn-primary w-full"
|
|
>
|
|
{loading ? (
|
|
<div className="flex items-center justify-center">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
Vytvára sa export...
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-center">
|
|
<DocumentArrowDownIcon className="h-5 w-5 mr-2" />
|
|
Vytvoriť export
|
|
</div>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Export History */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center">
|
|
<ClockIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">História exportov</h2>
|
|
</div>
|
|
<button
|
|
onClick={fetchExportJobs}
|
|
className="btn-secondary"
|
|
>
|
|
Obnoviť
|
|
</button>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Názov
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Typ
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Formát
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Vytvorený
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Veľkosť
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Akcie
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{exportJobs.map((job) => (
|
|
<tr key={job.id} className="table-row">
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{job.name}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<span className="capitalize">{job.type}</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<span className="uppercase">{job.format}</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
{getStatusIcon(job.status)}
|
|
<span className={`ml-2 inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(job.status)}`}>
|
|
{job.status}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{new Date(job.created_at).toLocaleString('sk-SK')}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{job.file_size || '-'}
|
|
{job.records_count && (
|
|
<div className="text-xs text-gray-400">{job.records_count} záznamov</div>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
|
{job.status === 'completed' && job.download_url && (
|
|
<a
|
|
href={job.download_url}
|
|
className="text-primary-600 hover:text-primary-900"
|
|
download
|
|
>
|
|
Stiahnuť
|
|
</a>
|
|
)}
|
|
{job.status === 'failed' && (
|
|
<button className="text-red-600 hover:text-red-900">
|
|
Zopakovať
|
|
</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
|
|
{exportJobs.length === 0 && (
|
|
<tr>
|
|
<td colSpan={7} className="px-6 py-12 text-center text-gray-500">
|
|
<DocumentArrowDownIcon className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
|
<p>Žiadne exporty</p>
|
|
<p className="text-sm">Vytvorte svoj prvý export dát</p>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Export Templates */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<Cog6ToothIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">Rýchle exporty</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<button className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 text-left">
|
|
<div className="text-sm font-medium text-gray-900">Všetky zdroje</div>
|
|
<div className="text-sm text-gray-500">CSV export všetkých zdrojov</div>
|
|
</button>
|
|
|
|
<button className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 text-left">
|
|
<div className="text-sm font-medium text-gray-900">Vysoké riziko</div>
|
|
<div className="text-sm text-gray-500">Zdroje s úrovňou rizika 4-5</div>
|
|
</button>
|
|
|
|
<button className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 text-left">
|
|
<div className="text-sm font-medium text-gray-900">Týždňový report</div>
|
|
<div className="text-sm text-gray-500">PDF report za posledný týždeň</div>
|
|
</button>
|
|
|
|
<button className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 text-left">
|
|
<div className="text-sm font-medium text-gray-900">Audit log</div>
|
|
<div className="text-sm text-gray-500">Záznamy za posledný mesiac</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|
|
|
|
export default ExportPage |