227 lines
8.9 KiB
TypeScript
227 lines
8.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Download, Upload, Loader2, Sliders, ShieldAlert, FileArchive } from 'lucide-react';
|
|
import { createClient } from '@/lib/supabase/client';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
export default function SystemPage() {
|
|
const [loading, setLoading] = useState(true);
|
|
const [demoActive, setDemoActive] = useState(true);
|
|
const [exporting, setExporting] = useState(false);
|
|
const [importing, setImporting] = useState(false);
|
|
const [statusMsg, setStatusMsg] = useState('');
|
|
const [statusType, setStatusType] = useState<'success' | 'error' | 'info' | ''>('');
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
async function loadData() {
|
|
try {
|
|
const supabase = createClient();
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (!user) {
|
|
router.push('/auth/login');
|
|
return;
|
|
}
|
|
const state = localStorage.getItem('demo_banner_disabled') !== 'true';
|
|
setDemoActive(state);
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
loadData();
|
|
}, [router]);
|
|
|
|
const toggleDemo = (checked: boolean) => {
|
|
setDemoActive(checked);
|
|
localStorage.setItem('demo_banner_disabled', (!checked).toString());
|
|
window.dispatchEvent(new Event('storage_demo_changed'));
|
|
};
|
|
|
|
const handleExport = async () => {
|
|
setExporting(true);
|
|
setStatusMsg('Export läuft...');
|
|
setStatusType('info');
|
|
try {
|
|
const res = await fetch('/api/admin/db-backup');
|
|
if (!res.ok) throw new Error('Export fehlgeschlagen');
|
|
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `webshop_backup_${new Date().toISOString().split('T')[0]}.zip`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
setStatusMsg('Export erfolgreich abgeschlossen.');
|
|
setStatusType('success');
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
setStatusMsg(`Fehler beim Export: ${err.message}`);
|
|
setStatusType('error');
|
|
} finally {
|
|
setExporting(false);
|
|
}
|
|
};
|
|
|
|
const handleImport = async () => {
|
|
if (!selectedFile) return;
|
|
if (!confirm('ACHTUNG: Dies löscht und überschreibt alle aktuellen Datenbankinhalte in dieser Instanz. Möchten Sie wirklich fortfahren?')) {
|
|
return;
|
|
}
|
|
setImporting(true);
|
|
setStatusMsg('Import läuft...');
|
|
setStatusType('info');
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', selectedFile);
|
|
|
|
const res = await fetch('/api/admin/db-backup', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Import fehlgeschlagen');
|
|
|
|
setStatusMsg('Datenbank erfolgreich importiert!');
|
|
setStatusType('success');
|
|
setSelectedFile(null);
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
setStatusMsg(`Fehler beim Import: ${err.message}`);
|
|
setStatusType('error');
|
|
} finally {
|
|
setImporting(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="p-8 text-white flex justify-center items-center"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>;
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 max-w-5xl mx-auto space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-extrabold tracking-tight text-white flex items-center gap-3">
|
|
<Sliders className="w-8 h-8 text-amber-400" />
|
|
System & Backup
|
|
</h1>
|
|
<p className="text-slate-400 text-xs mt-1">
|
|
Verwaltung von System-Bannern, Datenbank-Sicherungen und Wiederherstellungen.
|
|
</p>
|
|
</div>
|
|
|
|
{statusMsg && (
|
|
<div className={`p-3.5 rounded-xl text-xs font-medium border ${
|
|
statusType === 'success' ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400' :
|
|
statusType === 'error' ? 'bg-destructive/10 border-destructive/20 text-destructive' :
|
|
'bg-sky-500/10 border-sky-500/20 text-sky-400'
|
|
}`}>
|
|
{statusMsg}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<div className="p-5 rounded-2xl bg-slate-900/60 border border-slate-800 flex flex-col justify-between space-y-4">
|
|
<div className="space-y-3">
|
|
<div className="w-10 h-10 rounded-xl bg-sky-500/10 border border-sky-500/20 text-sky-400 flex items-center justify-center">
|
|
<Sliders className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-base text-white">System Banner</h3>
|
|
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
|
Gelbe Demo-Warnmeldungen im gesamten Shop aktivieren oder stummschalten.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between pt-3 border-t border-slate-800/80">
|
|
<span className="text-xs font-semibold text-slate-300">Banner aktiv</span>
|
|
<Switch checked={demoActive} onCheckedChange={toggleDemo} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-5 rounded-2xl bg-slate-900/60 border border-slate-800 flex flex-col justify-between space-y-4">
|
|
<div className="space-y-3">
|
|
<div className="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/20 text-blue-400 flex items-center justify-center">
|
|
<Download className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-base text-white">DB Backup</h3>
|
|
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
|
Lädt alle Produkte, Firmen, Lizenzen & Einstellungen als ZIP-Archiv herunter.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
onClick={handleExport}
|
|
disabled={exporting || importing}
|
|
className="w-full bg-blue-600 hover:bg-blue-500 text-white rounded-xl h-10 text-xs font-bold"
|
|
>
|
|
{exporting ? (
|
|
<><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Export läuft...</>
|
|
) : (
|
|
<><Download className="w-4 h-4 mr-2" /> ZIP Export</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="md:col-span-2 p-5 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-4">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="w-10 h-10 rounded-xl bg-amber-500/10 border border-amber-500/20 text-amber-400 flex items-center justify-center">
|
|
<Upload className="w-5 h-5" />
|
|
</div>
|
|
<span className="text-[10px] font-bold uppercase tracking-wider text-amber-400 bg-amber-500/10 px-2.5 py-1 rounded-full border border-amber-500/20 flex items-center gap-1">
|
|
<ShieldAlert className="w-3 h-3" /> Überschreibt DB
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-base text-white">Daten Wiederherstellung</h3>
|
|
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
|
Spielen Sie eine gesicherte ZIP-Sicherungsdatei ein. Warnung: Überschreibt aktuelle Datenbankinhalte.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid sm:grid-cols-2 gap-3 pt-2">
|
|
<div className="relative border border-dashed border-slate-700/80 rounded-xl p-3 hover:bg-slate-800/50 transition cursor-pointer flex items-center justify-center text-center">
|
|
<input
|
|
type="file"
|
|
accept=".zip"
|
|
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
|
disabled={exporting || importing}
|
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
|
/>
|
|
<span className="text-xs font-medium text-slate-300 truncate px-2 flex items-center gap-1.5">
|
|
<FileArchive className="w-4 h-4 text-amber-400 shrink-0" />
|
|
{selectedFile ? selectedFile.name : 'ZIP-Datei auswählen'}
|
|
</span>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleImport}
|
|
disabled={!selectedFile || exporting || importing}
|
|
variant="outline"
|
|
className="w-full border-amber-500/30 text-amber-300 hover:bg-amber-500/10 rounded-xl h-10 text-xs font-bold"
|
|
>
|
|
{importing ? (
|
|
<><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Importiert...</>
|
|
) : (
|
|
<><Upload className="w-4 h-4 mr-2" /> ZIP Einspielen</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|