- 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
205 lines
8.3 KiB
TypeScript
205 lines
8.3 KiB
TypeScript
import { useState } from 'react'
|
|
import type { NextPage } from 'next'
|
|
import Head from 'next/head'
|
|
import AdminLayout from '../../components/AdminLayout'
|
|
import { CloudArrowUpIcon, DocumentArrowUpIcon, CheckCircleIcon } from '@heroicons/react/24/outline'
|
|
|
|
const BulkImport: NextPage = () => {
|
|
const [file, setFile] = useState<File | null>(null)
|
|
const [importing, setImporting] = useState(false)
|
|
const [results, setResults] = useState<any>(null)
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (e.target.files && e.target.files[0]) {
|
|
setFile(e.target.files[0])
|
|
setResults(null)
|
|
}
|
|
}
|
|
|
|
const handleImport = async () => {
|
|
if (!file) return
|
|
|
|
setImporting(true)
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/bulk-import', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setResults(data)
|
|
setFile(null)
|
|
} else {
|
|
const error = await response.json()
|
|
alert('Chyba pri importe: ' + error.error)
|
|
}
|
|
} catch (error) {
|
|
alert('Chyba pri nahrávaní súboru')
|
|
} finally {
|
|
setImporting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>Hromadný import - Hliadka.sk Admin</title>
|
|
</Head>
|
|
<AdminLayout title="Hromadný import">
|
|
<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">
|
|
Importujte viacero zdrojov naraz pomocou CSV súboru
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 max-w-3xl">
|
|
<div className="card p-6">
|
|
<div className="text-center">
|
|
<CloudArrowUpIcon className="mx-auto h-12 w-12 text-gray-400" />
|
|
<h3 className="mt-2 text-sm font-semibold text-gray-900">Nahranie súboru</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Vyberte CSV súbor s URL adresami zdrojov na import
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<div className="flex justify-center px-6 py-10 border-2 border-gray-300 border-dashed rounded-md">
|
|
<div className="text-center">
|
|
<DocumentArrowUpIcon className="mx-auto h-12 w-12 text-gray-400" />
|
|
<div className="mt-4">
|
|
<label htmlFor="file-upload" className="cursor-pointer">
|
|
<span className="mt-2 block text-sm font-semibold text-gray-900">
|
|
Nahrať súbor
|
|
</span>
|
|
<input
|
|
id="file-upload"
|
|
name="file-upload"
|
|
type="file"
|
|
accept=".csv,.txt"
|
|
className="sr-only"
|
|
onChange={handleFileChange}
|
|
/>
|
|
</label>
|
|
<p className="text-sm text-gray-500">
|
|
CSV alebo TXT súbor až do 10MB
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{file && (
|
|
<div className="mt-4">
|
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
|
<div className="flex items-center">
|
|
<DocumentArrowUpIcon className="h-5 w-5 text-gray-400 mr-2" />
|
|
<span className="text-sm font-medium text-gray-900">{file.name}</span>
|
|
<span className="ml-2 text-sm text-gray-500">
|
|
({Math.round(file.size / 1024)} KB)
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setFile(null)}
|
|
className="text-sm text-red-600 hover:text-red-800"
|
|
>
|
|
Odstrániť
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{file && (
|
|
<div className="mt-6 flex justify-end">
|
|
<button
|
|
onClick={handleImport}
|
|
disabled={importing}
|
|
className="btn-primary"
|
|
>
|
|
{importing ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Importuje sa...
|
|
</>
|
|
) : (
|
|
<>
|
|
<CloudArrowUpIcon className="h-4 w-4 mr-2" />
|
|
Spustiť import
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{results && (
|
|
<div className="mt-8">
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-4">
|
|
<CheckCircleIcon className="h-6 w-6 text-green-500 mr-3" />
|
|
<h3 className="text-lg font-medium text-gray-900">Import dokončený</h3>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
<div className="bg-green-50 p-4 rounded-lg">
|
|
<div className="text-2xl font-bold text-green-600">{results.imported || 0}</div>
|
|
<div className="text-sm text-green-700">Úspešne importované</div>
|
|
</div>
|
|
<div className="bg-yellow-50 p-4 rounded-lg">
|
|
<div className="text-2xl font-bold text-yellow-600">{results.skipped || 0}</div>
|
|
<div className="text-sm text-yellow-700">Preskočené (duplikáty)</div>
|
|
</div>
|
|
<div className="bg-red-50 p-4 rounded-lg">
|
|
<div className="text-2xl font-bold text-red-600">{results.failed || 0}</div>
|
|
<div className="text-sm text-red-700">Neúspešné</div>
|
|
</div>
|
|
</div>
|
|
|
|
{results.errors && results.errors.length > 0 && (
|
|
<div className="mt-6">
|
|
<h4 className="text-sm font-medium text-gray-900 mb-2">Chyby pri importe:</h4>
|
|
<div className="bg-red-50 rounded-md p-3">
|
|
<ul className="text-sm text-red-700 space-y-1">
|
|
{results.errors.map((error: string, idx: number) => (
|
|
<li key={idx}>• {error}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-8">
|
|
<div className="card p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Inštrukcie</h3>
|
|
<div className="prose prose-sm text-gray-500">
|
|
<ul>
|
|
<li>CSV súbor by mal obsahovať jeden URL na riadok</li>
|
|
<li>Prvý riadok môže obsahovať hlavičku (bude preskočený)</li>
|
|
<li>Podporované formáty: .csv, .txt</li>
|
|
<li>Maximálna veľkosť súboru: 10MB</li>
|
|
<li>Duplikátne URL adresy budú automaticky preskočené</li>
|
|
<li>Neplatné URL adresy budú označené ako chyby</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default BulkImport |