Files
webshop/shop/app/admin/einstellungen/system/page.tsx
DanielS 5ea0719225
All checks were successful
Staging Build / build (push) Successful in 3m3s
feat(admin): restructure settings into subpages and hub overview
2026-08-18 22:55:20 +02:00

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 &amp; 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>
);
}