diff --git a/shop/app/admin/einstellungen/branding/page.tsx b/shop/app/admin/einstellungen/branding/page.tsx new file mode 100644 index 0000000..0607bd9 --- /dev/null +++ b/shop/app/admin/einstellungen/branding/page.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Loader2, Palette, Save } from 'lucide-react'; +import { createClient } from '@/lib/supabase/client'; +import { getBrandingSettings, saveBrandingSettings } from '@/lib/actions/branding'; +import type { BrandingSettings } from '@/lib/constants/branding'; +import { ColorThemePicker } from '@/components/admin/ColorThemePicker'; +import { useTheme } from '@/components/ThemeProvider'; +import { useRouter } from 'next/navigation'; + +export default function BrandingPage() { + const [loading, setLoading] = useState(true); + const [branding, setBranding] = useState({ + companyName: '', + street: '', + zip: '', + city: '', + billingStreet: '', + billingZip: '', + billingCity: '', + sameBillingAddress: true, + colorScheme: 'modern_blue', + primaryColor: '#2563eb', + accentColor: '#38bdf8', + }); + const [saving, setSaving] = useState(false); + const [msg, setMsg] = useState(''); + const [msgType, setMsgType] = useState<'success' | 'error' | ''>(''); + const { refreshBranding } = useTheme(); + 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 brandRes = await getBrandingSettings(); + if (brandRes) setBranding(brandRes); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + } + loadData(); + }, [router]); + + const handleSave = async () => { + setSaving(true); + setMsg(''); + try { + const res = await saveBrandingSettings(branding); + if (res.success) { + await refreshBranding(); + setMsgType('success'); + setMsg('Farbschema erfolgreich gespeichert.'); + } else { + setMsgType('error'); + setMsg(res.error || 'Fehler beim Speichern.'); + } + } catch (err: any) { + setMsgType('error'); + setMsg(err.message || 'Fehler beim Speichern.'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return
; + } + + return ( +
+
+
+

+ + Branding & Design +

+

+ Wählen Sie das primäre Farbschema und Akzente für den gesamten Webshop. +

+
+ + +
+ + {msg && ( +
+ {msg} +
+ )} + +
+ + setBranding({ + ...branding, + colorScheme: scheme, + primaryColor: primary, + accentColor: accent, + }) + } + /> +
+
+ ); +} diff --git a/shop/app/admin/einstellungen/firmendaten/page.tsx b/shop/app/admin/einstellungen/firmendaten/page.tsx new file mode 100644 index 0000000..6acec14 --- /dev/null +++ b/shop/app/admin/einstellungen/firmendaten/page.tsx @@ -0,0 +1,232 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Loader2, Building2, MapPin, Receipt, Save } from 'lucide-react'; +import { createClient } from '@/lib/supabase/client'; +import { getBrandingSettings, saveBrandingSettings } from '@/lib/actions/branding'; +import type { BrandingSettings } from '@/lib/constants/branding'; +import { useRouter } from 'next/navigation'; + +export default function FirmendatenPage() { + const [loading, setLoading] = useState(true); + const [branding, setBranding] = useState({ + companyName: '', + street: '', + zip: '', + city: '', + billingStreet: '', + billingZip: '', + billingCity: '', + sameBillingAddress: true, + colorScheme: 'modern_blue', + primaryColor: '#2563eb', + accentColor: '#38bdf8', + }); + const [saving, setSaving] = useState(false); + const [msg, setMsg] = useState(''); + const [msgType, setMsgType] = useState<'success' | 'error' | ''>(''); + 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 brandRes = await getBrandingSettings(); + if (brandRes) setBranding(brandRes); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + } + loadData(); + }, [router]); + + const handleSave = async () => { + setSaving(true); + setMsg(''); + try { + const res = await saveBrandingSettings(branding); + if (res.success) { + setMsgType('success'); + setMsg('Firmendaten erfolgreich gespeichert.'); + } else { + setMsgType('error'); + setMsg(res.error || 'Fehler beim Speichern.'); + } + } catch (err: any) { + setMsgType('error'); + setMsg(err.message || 'Fehler beim Speichern.'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return
; + } + + return ( +
+
+

+ + Firmendaten & Anschrift +

+

+ Verwaltung der eigenen Unternehmensanschrift für Rechnungen, Dokumente und Footereinträge. +

+
+ + {msg && ( +
+ {msg} +
+ )} + +
+
+
+ + setBranding({ ...branding, companyName: e.target.value })} + placeholder="z. B. Meine Firma GmbH" + className="bg-slate-950/80 border-slate-800 text-white text-sm" + /> +
+ +
+ + setBranding({ ...branding, logoUrl: e.target.value })} + placeholder="https://domain.de/logo.png oder /assets/logo.png" + className="bg-slate-950/80 border-slate-800 text-white text-sm" + /> +
+
+ +
+ + setBranding({ ...branding, developerFooter: e.target.value })} + placeholder="B2B Shop made by hephex" + className="bg-slate-950/80 border-slate-800 text-white text-sm" + /> +
+ +
+
+ + Hauptanschrift + +
+ + setBranding({ ...branding, street: e.target.value })} + placeholder="Musterstraße 12" + className="bg-slate-950/80 border-slate-800 text-white text-xs" + /> +
+
+
+ + setBranding({ ...branding, zip: e.target.value })} + placeholder="12345" + className="bg-slate-950/80 border-slate-800 text-white text-xs" + /> +
+
+ + setBranding({ ...branding, city: e.target.value })} + placeholder="Musterstadt" + className="bg-slate-950/80 border-slate-800 text-white text-xs" + /> +
+
+
+ +
+
+ + Rechnungsadresse + + +
+ + {!branding.sameBillingAddress ? ( +
+
+ + setBranding({ ...branding, billingStreet: e.target.value })} + placeholder="Rechnungsstraße 45" + className="bg-slate-950/80 border-slate-800 text-white text-xs" + /> +
+
+
+ + setBranding({ ...branding, billingZip: e.target.value })} + placeholder="54321" + className="bg-slate-950/80 border-slate-800 text-white text-xs" + /> +
+
+ + setBranding({ ...branding, billingCity: e.target.value })} + placeholder="Rechnungsstadt" + className="bg-slate-950/80 border-slate-800 text-white text-xs" + /> +
+
+
+ ) : ( +
+ Verwendet automatisch die Hauptanschrift oben. +
+ )} +
+
+ +
+ +
+
+
+ ); +} diff --git a/shop/app/admin/einstellungen/licserver/page.tsx b/shop/app/admin/einstellungen/licserver/page.tsx new file mode 100644 index 0000000..b76d196 --- /dev/null +++ b/shop/app/admin/einstellungen/licserver/page.tsx @@ -0,0 +1,162 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Loader2, KeyRound, Server, CheckCircle2, XCircle, Eye, EyeOff, Wifi } from 'lucide-react'; +import { saveLicServerConfig, testLicServerConnection } from '@/lib/actions/licserver-config'; +import { createClient } from '@/lib/supabase/client'; +import { useRouter } from 'next/navigation'; + +export default function LicServerPage() { + const [loading, setLoading] = useState(true); + const [licUrl, setLicUrl] = useState(''); + const [licKey, setLicKey] = useState(''); + const [showKey, setShowKey] = useState(false); + const [licSaving, setLicSaving] = useState(false); + const [licTesting, setLicTesting] = useState(false); + const [licStatus, setLicStatus] = useState<{ ok: boolean; message: string } | null>(null); + const [licMsg, setLicMsg] = useState(''); + const [licMsgType, setLicMsgType] = useState<'success' | 'error' | ''>(''); + 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 { data } = await supabase.from('settings').select('licserver_base_url, licserver_api_key').eq('id', 'licserver').maybeSingle(); + if (data) { + setLicUrl(data.licserver_base_url || ''); + setLicKey(data.licserver_api_key || ''); + } + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + } + loadData(); + }, [router]); + + if (loading) { + return
; + } + + return ( +
+
+
+

+ + CASPOS LicServer +

+

+ REST API-Anbindung für die automatisierte Lizenzierung und Schlüsselgenerierung. +

+
+ + {licStatus && ( +
+ {licStatus.ok ? : } + {licStatus.message} +
+ )} +
+ + {licMsg && ( +
+ {licMsg} +
+ )} + +
+
+
+ + setLicUrl(e.target.value)} + placeholder="http://192.168.178.174:9980" + className="text-sm font-mono bg-slate-950/80 border-slate-800 text-white rounded-xl" + /> +
+ +
+ +
+ setLicKey(e.target.value)} + placeholder="Ihr X-Api-Key" + className="text-sm font-mono pr-10 bg-slate-950/80 border-slate-800 text-white rounded-xl" + /> + +
+
+
+ +
+ + + +
+
+
+ ); +} diff --git a/shop/app/admin/einstellungen/page.tsx b/shop/app/admin/einstellungen/page.tsx index ab987a9..bd48ac1 100644 --- a/shop/app/admin/einstellungen/page.tsx +++ b/shop/app/admin/einstellungen/page.tsx @@ -1,85 +1,57 @@ -/* Admin Settings with Bento Grid Layout & Motion Animations */ 'use client'; -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; import { motion } from 'framer-motion'; -import { Switch } from '@/components/ui/switch'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { - Download, Upload, Database, Loader2, KeyRound, - Server, CheckCircle2, XCircle, Eye, EyeOff, Wifi, Sliders, ShieldAlert, FileArchive, - Building2, MapPin, Receipt, Palette, Save -} from 'lucide-react'; +import { Building2, Palette, KeyRound, Mail, Sliders, ArrowRight, Loader2 } from 'lucide-react'; import { createClient } from '@/lib/supabase/client'; -import { saveLicServerConfig, testLicServerConnection } from '@/lib/actions/licserver-config'; -import { getBrandingSettings, saveBrandingSettings } from '@/lib/actions/branding'; -import type { BrandingSettings } from '@/lib/constants/branding'; -import { ColorThemePicker } from '@/components/admin/ColorThemePicker'; -import { useTheme } from '@/components/ThemeProvider'; import { useRouter } from 'next/navigation'; -const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1 - } - } -}; +const menuItems = [ + { + title: 'Firmendaten & Anschrift', + description: 'Verwaltung der eigenen Firmenanschrift, Rechnungsadresse und Sublines für Dokumente.', + href: '/admin/einstellungen/firmendaten', + icon: Building2, + color: 'text-blue-400', + bgColor: 'bg-blue-500/10 border-blue-500/20', + }, + { + title: 'Branding & Farbschema', + description: 'Anpassung von Farbschemata (Primary / Accent), Buttons und Themes für den Webshop.', + href: '/admin/einstellungen/branding', + icon: Palette, + color: 'text-violet-400', + bgColor: 'bg-violet-500/10 border-violet-500/20', + }, + { + title: 'CASPOS LicServer', + description: 'REST API Konfiguration für den Lizenzserver & automatisierte Schlüsselgenerierung.', + href: '/admin/einstellungen/licserver', + icon: KeyRound, + color: 'text-amber-400', + bgColor: 'bg-amber-500/10 border-amber-500/20', + }, + { + title: 'SMTP E-Mail Server', + description: 'Serverdaten & Authentifizierung für Bestellbestätigungen und Benachrichtigungen.', + href: '/admin/settings', + icon: Mail, + color: 'text-sky-400', + bgColor: 'bg-sky-500/10 border-sky-500/20', + }, + { + title: 'System & Backup', + description: 'Demo-Banner Steuerung sowie ZIP-Export und Wiederherstellung der Datenbank.', + href: '/admin/einstellungen/system', + icon: Sliders, + color: 'text-emerald-400', + bgColor: 'bg-emerald-500/10 border-emerald-500/20', + }, +]; -const cardVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { - type: 'spring' as const, - stiffness: 80, - damping: 15 - } - } -}; - -export default function AdminSettings() { - 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(null); +export default function AdminSettingsOverview() { const [loading, setLoading] = useState(true); - - // LicServer Config State - const [licUrl, setLicUrl] = useState(''); - const [licKey, setLicKey] = useState(''); - const [showKey, setShowKey] = useState(false); - const [licSaving, setLicSaving] = useState(false); - const [licTesting, setLicTesting] = useState(false); - const [licStatus, setLicStatus] = useState<{ ok: boolean; message: string } | null>(null); - const [licMsg, setLicMsg] = useState(''); - const [licMsgType, setLicMsgType] = useState<'success' | 'error' | ''>(''); - - // Branding State - const [branding, setBranding] = useState({ - companyName: '', - street: '', - zip: '', - city: '', - billingStreet: '', - billingZip: '', - billingCity: '', - sameBillingAddress: true, - colorScheme: 'modern_blue', - primaryColor: '#2563eb', - accentColor: '#38bdf8', - }); - const [brandingSaving, setBrandingSaving] = useState(false); - const [brandingMsg, setBrandingMsg] = useState(''); - const [brandingMsgType, setBrandingMsgType] = useState<'success' | 'error' | ''>(''); - const router = useRouter(); useEffect(() => { @@ -91,31 +63,13 @@ export default function AdminSettings() { router.push('/auth/login'); return; } - - const [userRes, licRes, brandRes] = await Promise.all([ - supabase.from('users').select('role').eq('id', user.id).single(), - supabase.from('settings').select('licserver_base_url, licserver_api_key').eq('id', 'licserver').maybeSingle(), - getBrandingSettings() - ]); - - if (userRes.error || !userRes.data || userRes.data.role === 'verwaltung') { + const { data } = await supabase.from('users').select('role').eq('id', user.id).single(); + if (!data || data.role === 'verwaltung') { router.push('/admin'); return; } - - const state = localStorage.getItem('demo_banner_disabled') !== 'true'; - setDemoActive(state); - - if (licRes.data) { - setLicUrl(licRes.data.licserver_base_url || ''); - setLicKey(licRes.data.licserver_api_key || ''); - } - - if (brandRes) { - setBranding(brandRes); - } } catch (err) { - console.error("Fehler bei checkAccess in Einstellungen:", err); + console.error(err); } finally { setLoading(false); } @@ -123,565 +77,51 @@ export default function AdminSettings() { checkAccess(); }, [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); - } - }; - - const { refreshBranding } = useTheme(); - - const handleSaveBranding = async () => { - setBrandingSaving(true); - setBrandingMsg(''); - try { - const res = await saveBrandingSettings(branding); - if (res.success) { - await refreshBranding(); - setBrandingMsgType('success'); - setBrandingMsg('Firmendaten & Farbschema erfolgreich gespeichert.'); - } else { - setBrandingMsgType('error'); - setBrandingMsg(res.error || 'Fehler beim Speichern.'); - } - } catch (err: any) { - setBrandingMsgType('error'); - setBrandingMsg(err.message || 'Fehler beim Speichern.'); - } finally { - setBrandingSaving(false); - } - }; - if (loading) { return
; } return ( -
- {/* Header */} - -
-

Admin Einstellungen

-

- Zentrales Dashboard für Firmendaten, Branding, Backup & Lizenz-Konfigurationen. -

-
+
+ +

Systemeinstellungen

+

+ Wählen Sie eine Kategorie aus, um Konfigurationen, Server-Anbindungen oder Branding anzupassen. +

- {/* Dynamic Status Notification */} - {statusMsg && ( - - {statusMsg} - - )} - - {/* Bento Grid Container */} - - {/* Bento Item 1: System Modus & Banner */} - -
-
- -
-
-

System Banner

-

- Gelbe Demo-Warnmeldungen im gesamten Shop aktivieren oder stummschalten. -

-
-
-
- Banner aktiv - -
-
- - {/* Bento Item 2: Daten Export */} - -
-
- -
-
-

DB Backup

-

- Lädt alle Produkte, Firmen, Lizenzen & Einstellungen als ZIP-Archiv herunter. -

-
-
- -
- - {/* Bento Item 3: Daten Import */} - -
-
-
- -
- - Überschreibt DB - -
-
-

Daten Wiederherstellung

-

- Spielen Sie eine gesicherte ZIP-Sicherungsdatei ein. Warnung: Überschreibt aktuelle Datenbankinhalte. -

-
-
- -
-
- 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" - /> - - - {selectedFile ? selectedFile.name : 'ZIP-Datei auswählen'} - -
- - -
-
- - {/* Bento Item 4: Firmendaten & Anschriften (4 Spalten) */} - -
-
-
- -
-
-

Firmendaten & Rechnungsadresse

-

Verwaltung der eigenen Unternehmensanschrift für Rechnungen und Dokumente

-
-
- - -
- - {brandingMsg && ( -
- {brandingMsg} -
- )} - -
-
-
- - setBranding({ ...branding, companyName: e.target.value })} - placeholder="z. B. Meine Firma GmbH" - className="bg-slate-950/80 border-slate-800 text-white text-sm" - /> -
- -
- - setBranding({ ...branding, logoUrl: e.target.value })} - placeholder="https://domain.de/logo.png oder /assets/logo.png" - className="bg-slate-950/80 border-slate-800 text-white text-sm" - /> -
-
- -
- - setBranding({ ...branding, developerFooter: e.target.value })} - placeholder="B2B Shop made by hephex" - className="bg-slate-950/80 border-slate-800 text-white text-sm" - /> -
- - {/* Anschrift */} -
-
- - Hauptanschrift - -
- - setBranding({ ...branding, street: e.target.value })} - placeholder="Musterstraße 12" - className="bg-slate-950/80 border-slate-800 text-white text-xs" - /> -
-
+
+ {menuItems.map((item, idx) => { + const Icon = item.icon; + return ( + + +
+
+ +
- - setBranding({ ...branding, zip: e.target.value })} - placeholder="12345" - className="bg-slate-950/80 border-slate-800 text-white text-xs" - /> -
-
- - setBranding({ ...branding, city: e.target.value })} - placeholder="Musterstadt" - className="bg-slate-950/80 border-slate-800 text-white text-xs" - /> +

+ {item.title} +

+

+ {item.description} +

-
- {/* Rechnungsadresse */} -
-
- - Rechnungsadresse - - +
+ Einstellungen öffnen +
- - {!branding.sameBillingAddress ? ( -
-
- - setBranding({ ...branding, billingStreet: e.target.value })} - placeholder="Rechnungsstraße 45" - className="bg-slate-950/80 border-slate-800 text-white text-xs" - /> -
-
-
- - setBranding({ ...branding, billingZip: e.target.value })} - placeholder="54321" - className="bg-slate-950/80 border-slate-800 text-white text-xs" - /> -
-
- - setBranding({ ...branding, billingCity: e.target.value })} - placeholder="Rechnungsstadt" - className="bg-slate-950/80 border-slate-800 text-white text-xs" - /> -
-
-
- ) : ( -
- Verwendet automatisch die Hauptanschrift oben. -
- )} -
-
-
- - - {/* Bento Item 5: Farbschema & Design-System (4 Spalten) */} - -
-
-
- -
-
-

Farbschema & Webshop Styling

-

Wählen Sie das primäre Farbschema für Buttons, Akzente und Highlights

-
-
- - -
- - - setBranding({ - ...branding, - colorScheme: scheme, - primaryColor: primary, - accentColor: accent, - }) - } - /> -
- - {/* Bento Item 6: LicServer Konfiguration */} - -
-
-
- -
-
-

CASPOS Lizenzserver (LicServer)

-

REST API-Anbindung für automatisierte Lizenzierung

-
-
- - {licStatus && ( -
- {licStatus.ok ? : } - {licStatus.message} -
- )} -
- - {licMsg && ( -
- {licMsg} -
- )} - -
-
- - setLicUrl(e.target.value)} - placeholder="http://192.168.178.174:9980" - className="text-sm font-mono bg-slate-950/80 border-slate-800 text-white rounded-xl" - /> -
- -
- -
- setLicKey(e.target.value)} - placeholder="Ihr X-Api-Key" - className="text-sm font-mono pr-10 bg-slate-950/80 border-slate-800 text-white rounded-xl" - /> - -
-
-
- -
- - - -
-
- + + + ); + })} +
); } diff --git a/shop/app/admin/einstellungen/system/page.tsx b/shop/app/admin/einstellungen/system/page.tsx new file mode 100644 index 0000000..a6bb120 --- /dev/null +++ b/shop/app/admin/einstellungen/system/page.tsx @@ -0,0 +1,226 @@ +'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(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
; + } + + return ( +
+
+

+ + System & Backup +

+

+ Verwaltung von System-Bannern, Datenbank-Sicherungen und Wiederherstellungen. +

+
+ + {statusMsg && ( +
+ {statusMsg} +
+ )} + +
+
+
+
+ +
+
+

System Banner

+

+ Gelbe Demo-Warnmeldungen im gesamten Shop aktivieren oder stummschalten. +

+
+
+
+ Banner aktiv + +
+
+ +
+
+
+ +
+
+

DB Backup

+

+ Lädt alle Produkte, Firmen, Lizenzen & Einstellungen als ZIP-Archiv herunter. +

+
+
+ +
+ +
+
+
+
+ +
+ + Überschreibt DB + +
+
+

Daten Wiederherstellung

+

+ Spielen Sie eine gesicherte ZIP-Sicherungsdatei ein. Warnung: Überschreibt aktuelle Datenbankinhalte. +

+
+
+ +
+
+ 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" + /> + + + {selectedFile ? selectedFile.name : 'ZIP-Datei auswählen'} + +
+ + +
+
+
+
+ ); +} diff --git a/shop/app/admin/layout.tsx b/shop/app/admin/layout.tsx index b53d84e..b22e5e7 100644 --- a/shop/app/admin/layout.tsx +++ b/shop/app/admin/layout.tsx @@ -77,17 +77,25 @@ export default async function AdminLayout({
Einstellungen
- - - Allgemein + + + Firmendaten + + + + Branding & Design + + + + LicServer - - SMTP + + SMTP Server - - - DB-Tools + + + System & Backup )} diff --git a/shop/components/admin/orders-table.tsx b/shop/components/admin/orders-table.tsx index a9379e5..f9590ab 100644 --- a/shop/components/admin/orders-table.tsx +++ b/shop/components/admin/orders-table.tsx @@ -545,128 +545,150 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
- {/* Geräte & Produktauswahl (Snapshot Aufteilung) */} -
-
-
- - Konfigurierte Kassen & Module ({displayItems.length}) -
- {selectedOrder.status === 'in_review' && !isEditingSnapshot && ( - - )} -
+ {/* Geräte & Produktauswahl (Nach Kassen / Boxen gruppiert) */} +
+
+
+ + Konfigurierte Kassen ({displayItems.length}) +
+ {selectedOrder.status === 'in_review' && !isEditingSnapshot && ( + + )} +
- {isEditingSnapshot && ( -
- Sie befinden sich im Preiskorrektur-Modus. + {isEditingSnapshot && ( +
+ Sie befinden sich im Preiskorrektur-Modus. +
+ + +
+
+ )} + +
+ {displayItems.map((item: any, idx: number) => { + const devName = item.device_name || `Kasse #${idx + 1}` + const licNum = item.license_number + const modules = item.selected_modules || [] + + return ( +
+ {/* Box Header für die Kasse */} +
- - -
-
- )} - -
- {displayItems.map((item: any, idx: number) => ( -
-
-
- - {item.device_name || `Kasse #${idx + 1}`} - - {item.product_name} -
- - {isEditingSnapshot ? ( -
- - updateItemBasePrice(idx, parseFloat(e.target.value) || 0)} - className="w-20 h-7 bg-slate-950 border-white/10 text-white text-xs px-2" - /> -
- ) : ( - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.base_price || 0)} - - )} -
- - {/* Ausgewählte Module & Lizenzen */} - {item.selected_modules?.length > 0 && ( -
-
- - Zusatzmodule ({item.selected_modules.length}): -
-
- {item.selected_modules.map((mod: any, mIdx: number) => ( -
- - {mod.module_name || mod.name} - - - {isEditingSnapshot ? ( -
- updateModulePriceOrQty(idx, mIdx, 'price', parseFloat(e.target.value) || 0)} - className="w-14 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1" - /> - x - updateModulePriceOrQty(idx, mIdx, 'quantity', parseInt(e.target.value) || 1)} - className="w-10 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1" - /> -
- ) : ( - - +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * (mod.quantity || 1))} - - )} -
- ))} -
-
+ + {devName} + + {licNum && ( + + Lizenz: + {licNum} + )}
- ))} + + {item.billing_interval === 'one_time' ? 'Kauf' : 'Abo'} + +
+ + {/* Inhalt der Kasse: Hauptprodukt + Preis */} +
+ {item.product_name} + {isEditingSnapshot ? ( +
+ + updateItemBasePrice(idx, parseFloat(e.target.value) || 0)} + className="w-20 h-7 bg-slate-950 border-white/10 text-white text-xs px-2 font-mono" + /> +
+ ) : ( + (item.price || item.base_price) && ( + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.price || item.base_price)} + + ) + )} +
+ + {/* Module dieser Kasse */} + {modules.length > 0 && ( +
+
+ + Enthaltene Module ({modules.length}): +
+
+ {modules.map((mod: any, mIdx: number) => ( +
+ + + {mod.module_name || mod.name} {!isEditingSnapshot && mod.quantity > 1 ? `(${mod.quantity}x)` : ''} + + {isEditingSnapshot ? ( +
+ updateModulePriceOrQty(idx, mIdx, 'price', parseFloat(e.target.value) || 0)} + className="w-14 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1 font-mono" + /> + x + updateModulePriceOrQty(idx, mIdx, 'quantity', parseInt(e.target.value) || 1)} + className="w-10 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1 font-mono" + /> +
+ ) : ( + mod.price && ( + + +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * (mod.quantity || 1))} + + ) + )} +
+ ))} +
+
+ )}
-
+ ) + })} +
+
{/* Workflow Freigabe & Aktionen */}
diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 367f68b..28abcc0 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -429,11 +429,15 @@ export function OrderWizard({ }) }, [visibleCategories, selections, products, selectedBillingInterval]) - const finalItemsToShow = basketItems.length > 0 - ? basketItems - : (allCategoriesFilled && productValidationErrors.length === 0 - ? [{ deviceName: deviceName || 'Kasse 1', selections, moduleQuantities, billingInterval: selectedBillingInterval }] - : []) + const finalItemsToShow = useMemo(() => { + if (basketItems.length > 0) { + return basketItems + } + if (allCategoriesFilled && productValidationErrors.length === 0 && hasActiveSelection) { + return [{ deviceName: deviceName || 'Kasse 1', licenseNumber, selections, moduleQuantities, billingInterval: selectedBillingInterval }] + } + return [] + }, [basketItems, allCategoriesFilled, productValidationErrors, hasActiveSelection, deviceName, licenseNumber, selections, moduleQuantities, selectedBillingInterval]) // Gesamtsummen für alle Items in der Bestellung (finalItemsToShow) const { overallMonthlyTotal, overallOneTimeTotal, linkedFeeProducts } = useMemo(() => { @@ -542,7 +546,7 @@ export function OrderWizard({ const nextStep = () => { if (step === 3 && allCategoriesFilled && productValidationErrors.length === 0) { - const isDirtyNewDevice = deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0) + const isDirtyNewDevice = (deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0)) && editingIdx === null if (editingIdx !== null) { addToBasket() } else if (basketItems.length === 0 && hasActiveSelection) { @@ -743,35 +747,35 @@ export function OrderWizard({ setIsSubmitting(true) try { let finalItems = [...basketItems] - const isDirtyNewDevice = deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0) - if ((editingIdx !== null || basketItems.length === 0 || isDirtyNewDevice) && hasActiveSelection && allCategoriesFilled && productValidationErrors.length === 0) { + const isDirtyNewDevice = (deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0)) && editingIdx === null + if (editingIdx !== null) { const normLicense = licenseNumber.trim().toUpperCase() - if (normLicense) { - const isDuplicateInBasket = basketItems.some( - (item, idx) => idx !== editingIdx && item.licenseNumber?.toUpperCase().trim() === normLicense - ) - if (isDuplicateInBasket) { - throw new Error(`Lizenznummer "${licenseNumber}" ist bereits im aktuellen Warenkorb vorhanden.`) - } - - const isTaken = await isLicenseNumberTaken(normLicense, initialOrder?.id) - if (isTaken) { - throw new Error(`Lizenznummer "${licenseNumber}" wird bereits von einer anderen Bestellung verwendet.`) - } - } - const currentItem = { - deviceName: deviceName || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`), + deviceName: deviceName || basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`, licenseNumber: normLicense, selections: JSON.parse(JSON.stringify(selections)), moduleQuantities: { ...moduleQuantities }, billingInterval: selectedBillingInterval, } - if (editingIdx !== null) { - finalItems[editingIdx] = currentItem - } else { - finalItems.push(currentItem) - } + finalItems[editingIdx] = currentItem + } else if (basketItems.length === 0 && hasActiveSelection && allCategoriesFilled && productValidationErrors.length === 0) { + const normLicense = licenseNumber.trim().toUpperCase() + finalItems.push({ + deviceName: deviceName || 'Kasse 1', + licenseNumber: normLicense, + selections: JSON.parse(JSON.stringify(selections)), + moduleQuantities: { ...moduleQuantities }, + billingInterval: selectedBillingInterval, + }) + } else if (isDirtyNewDevice && hasActiveSelection && allCategoriesFilled && productValidationErrors.length === 0) { + const normLicense = licenseNumber.trim().toUpperCase() + finalItems.push({ + deviceName: deviceName || `Kasse ${basketItems.length + 1}`, + licenseNumber: normLicense, + selections: JSON.parse(JSON.stringify(selections)), + moduleQuantities: { ...moduleQuantities }, + billingInterval: selectedBillingInterval, + }) } if (finalItems.length === 0) {