- Refactoring: Error Boundaries, apiFetch Wrapper, Socket Status-Tracking - Refactoring: UI Kontrast (theme-aware colors), unused imports bereinigt - Symbol-Verwaltung: Neues Split-Panel (Meine Symbole + Bibliothek) - Symbol-Verwaltung: Umbenennen (TLF rot/blau), Duplikate erlaubt - Symbol-Verwaltung: Karten-Sidebar zeigt eigene Symbole bevorzugt - Schlauch-Labels: Groessere Schrift (13px/10px), verschiebbar (Drag) - Schema: TenantSymbol customName, sortOrder, unique constraint entfernt - Open Source Referenz entfernt (kostenloses Projekt)
451 lines
22 KiB
TypeScript
451 lines
22 KiB
TypeScript
'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 { useToast } from '@/components/ui/use-toast'
|
|
import Link from 'next/link'
|
|
import {
|
|
Mail, Send, CheckCircle, Ban, CreditCard, Map, MapPin, Settings,
|
|
Shield, UserPlus, ArrowLeft, Loader2,
|
|
} from 'lucide-react'
|
|
|
|
interface SettingsTabProps {
|
|
usersCount: number
|
|
tenantsCount: number
|
|
iconsCount: number
|
|
onNavigateTab: (tab: string) => void
|
|
}
|
|
|
|
export function SettingsTab({ usersCount, tenantsCount, iconsCount, onNavigateTab }: SettingsTabProps) {
|
|
const { toast } = useToast()
|
|
|
|
// SMTP Settings
|
|
const [smtpHost, setSmtpHost] = useState('')
|
|
const [smtpPort, setSmtpPort] = useState('587')
|
|
const [smtpSecure, setSmtpSecure] = useState(false)
|
|
const [smtpUser, setSmtpUser] = useState('')
|
|
const [smtpPass, setSmtpPass] = useState('')
|
|
const [smtpFromName, setSmtpFromName] = useState('Lageplan')
|
|
const [smtpFromEmail, setSmtpFromEmail] = useState('')
|
|
const [smtpTestEmail, setSmtpTestEmail] = useState('')
|
|
const [smtpLoading, setSmtpLoading] = useState(false)
|
|
const [smtpStatus, setSmtpStatus] = useState<string | null>(null)
|
|
const [contactEmail, setContactEmail] = useState('app@lageplan.ch')
|
|
const [notifyRegistrationEmail, setNotifyRegistrationEmail] = useState('')
|
|
|
|
// Stripe Settings
|
|
const [stripePublicKey, setStripePublicKey] = useState('')
|
|
const [stripeSecretKey, setStripeSecretKey] = useState('')
|
|
const [stripeWebhookSecret, setStripeWebhookSecret] = useState('')
|
|
const [stripeLoading, setStripeLoading] = useState(false)
|
|
const [stripeStatus, setStripeStatus] = useState<string | null>(null)
|
|
|
|
// Demo Project
|
|
const [demoProjectId, setDemoProjectId] = useState('')
|
|
const [allProjects, setAllProjects] = useState<{ id: string; title: string; location?: string }[]>([])
|
|
const [demoLoading, setDemoLoading] = useState(false)
|
|
const [demoStatus, setDemoStatus] = useState<string | null>(null)
|
|
|
|
// Default Symbol Scale
|
|
const [defaultSymbolScale, setDefaultSymbolScale] = useState('1.5')
|
|
const [symbolScaleLoading, setSymbolScaleLoading] = useState(false)
|
|
const [symbolScaleStatus, setSymbolScaleStatus] = useState<string | null>(null)
|
|
|
|
// Load settings on mount
|
|
useEffect(() => {
|
|
fetch('/api/admin/settings').then(r => r.json()).then(data => {
|
|
if (data.smtp) {
|
|
setSmtpHost(data.smtp.host || '')
|
|
setSmtpPort(data.smtp.port?.toString() || '587')
|
|
setSmtpSecure(data.smtp.secure || false)
|
|
setSmtpUser(data.smtp.user || '')
|
|
setSmtpFromName(data.smtp.fromName || 'Lageplan')
|
|
setSmtpFromEmail(data.smtp.fromEmail || '')
|
|
}
|
|
if (data.stripe) {
|
|
setStripePublicKey(data.stripe.publicKey || '')
|
|
setStripeSecretKey(data.stripe.secretKey ? '••••••••' : '')
|
|
setStripeWebhookSecret(data.stripe.webhookSecret ? '••••••••' : '')
|
|
}
|
|
if (data.contactEmail) setContactEmail(data.contactEmail)
|
|
if (data.notifyRegistrationEmail) setNotifyRegistrationEmail(data.notifyRegistrationEmail)
|
|
if (data.demoProjectId) setDemoProjectId(data.demoProjectId)
|
|
if (data.defaultSymbolScale) setDefaultSymbolScale(data.defaultSymbolScale.toString())
|
|
}).catch(() => {})
|
|
|
|
// Load projects for demo selector
|
|
fetch('/api/projects').then(r => r.json()).then(data => {
|
|
if (data.projects) setAllProjects(data.projects)
|
|
}).catch(() => {})
|
|
}, [])
|
|
|
|
const handleSmtpSave = async () => {
|
|
setSmtpLoading(true)
|
|
setSmtpStatus(null)
|
|
try {
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'save_smtp',
|
|
smtp: { host: smtpHost, port: parseInt(smtpPort), secure: smtpSecure, user: smtpUser, pass: smtpPass, fromName: smtpFromName, fromEmail: smtpFromEmail },
|
|
}),
|
|
})
|
|
const data = await res.json()
|
|
if (data.success) {
|
|
toast({ title: 'SMTP gespeichert' })
|
|
setSmtpStatus('saved')
|
|
} else throw new Error(data.error)
|
|
} catch (error) {
|
|
toast({ title: 'Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' })
|
|
} finally { setSmtpLoading(false) }
|
|
}
|
|
|
|
const handleSmtpTest = async () => {
|
|
setSmtpLoading(true)
|
|
setSmtpStatus(null)
|
|
try {
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'test_smtp' }),
|
|
})
|
|
const data = await res.json()
|
|
if (data.success) {
|
|
setSmtpStatus('connected')
|
|
toast({ title: 'SMTP-Verbindung erfolgreich' })
|
|
} else {
|
|
setSmtpStatus('error')
|
|
toast({ title: 'Verbindung fehlgeschlagen', description: data.error, variant: 'destructive' })
|
|
}
|
|
} catch (error) {
|
|
setSmtpStatus('error')
|
|
toast({ title: 'Fehler', variant: 'destructive' })
|
|
} finally { setSmtpLoading(false) }
|
|
}
|
|
|
|
const handleSmtpSendTest = async () => {
|
|
if (!smtpTestEmail) return
|
|
setSmtpLoading(true)
|
|
try {
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'send_test_email', testEmail: smtpTestEmail }),
|
|
})
|
|
const data = await res.json()
|
|
if (data.success) toast({ title: data.message })
|
|
else toast({ title: 'Senden fehlgeschlagen', description: data.error, variant: 'destructive' })
|
|
} catch { toast({ title: 'Fehler', variant: 'destructive' }) }
|
|
finally { setSmtpLoading(false) }
|
|
}
|
|
|
|
const handleContactEmailSave = async () => {
|
|
setSmtpLoading(true)
|
|
try {
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'save_contact_email', contactEmail }),
|
|
})
|
|
const data = await res.json()
|
|
if (data.success) toast({ title: 'Kontakt-E-Mail gespeichert' })
|
|
else throw new Error(data.error)
|
|
} catch (error) {
|
|
toast({ title: 'Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' })
|
|
} finally { setSmtpLoading(false) }
|
|
}
|
|
|
|
const handleStripeSave = async () => {
|
|
setStripeLoading(true)
|
|
setStripeStatus(null)
|
|
try {
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'save_stripe',
|
|
stripe: {
|
|
publicKey: stripePublicKey,
|
|
secretKey: stripeSecretKey,
|
|
webhookSecret: stripeWebhookSecret,
|
|
},
|
|
}),
|
|
})
|
|
const data = await res.json()
|
|
if (data.success) {
|
|
setStripeStatus('saved')
|
|
toast({ title: 'Stripe-Einstellungen gespeichert' })
|
|
} else throw new Error(data.error)
|
|
} catch (error) {
|
|
setStripeStatus('error')
|
|
toast({ title: 'Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' })
|
|
} finally { setStripeLoading(false) }
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Contact Email */}
|
|
<div className="border rounded-lg p-6 md:col-span-2">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<Mail className="w-5 h-5 text-primary" />
|
|
Kontakt-E-Mail
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-3">E-Mail-Adresse für das Kontaktformular auf der Landing Page. Hierhin werden Anfragen gesendet.</p>
|
|
<div className="flex gap-2 items-end">
|
|
<div className="flex-1"><Label>Empfänger-Adresse</Label><Input value={contactEmail} onChange={e => setContactEmail(e.target.value)} placeholder="app@lageplan.ch" /></div>
|
|
<Button onClick={handleContactEmailSave} disabled={smtpLoading}>Speichern</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Registration Notification */}
|
|
<div className="border rounded-lg p-6">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<Mail className="w-5 h-5 text-primary" />
|
|
Registrierungs-Benachrichtigung
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-3">E-Mail-Adresse, an die bei neuen Registrierungen eine Benachrichtigung gesendet wird. Leer lassen = keine Benachrichtigung.</p>
|
|
<div className="flex gap-2 items-end">
|
|
<div className="flex-1"><Label>Admin-E-Mail</Label><Input value={notifyRegistrationEmail} onChange={e => setNotifyRegistrationEmail(e.target.value)} placeholder="admin@lageplan.ch" /></div>
|
|
<Button onClick={async () => {
|
|
try {
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'save_setting', key: 'notify_registration_email', value: notifyRegistrationEmail }),
|
|
})
|
|
if ((await res.json()).success) toast({ title: 'Gespeichert' })
|
|
} catch { toast({ title: 'Fehler', variant: 'destructive' }) }
|
|
}} disabled={smtpLoading}>Speichern</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SMTP Settings */}
|
|
<div className="border rounded-lg p-6 md:col-span-2">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<Mail className="w-5 h-5 text-primary" />
|
|
E-Mail / SMTP Konfiguration
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">SMTP-Server für den E-Mail-Versand konfigurieren. Empfohlen: TLS auf Port 587. Passwörter werden verschlüsselt in der Datenbank gespeichert.</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div><Label>SMTP Host</Label><Input value={smtpHost} onChange={e => setSmtpHost(e.target.value)} placeholder="smtp.gmail.com" /></div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div><Label>Port</Label><Input value={smtpPort} onChange={e => setSmtpPort(e.target.value)} placeholder="587" /></div>
|
|
<div className="flex items-end gap-2 pb-0.5">
|
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
|
<input type="checkbox" checked={smtpSecure} onChange={e => setSmtpSecure(e.target.checked)} className="rounded" />
|
|
SSL/TLS
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div><Label>Benutzername</Label><Input value={smtpUser} onChange={e => setSmtpUser(e.target.value)} placeholder="user@example.com" /></div>
|
|
<div><Label>Passwort</Label><Input type="password" value={smtpPass} onChange={e => setSmtpPass(e.target.value)} placeholder="App-Passwort oder SMTP-Passwort" /></div>
|
|
<div><Label>Absender-Name</Label><Input value={smtpFromName} onChange={e => setSmtpFromName(e.target.value)} placeholder="Lageplan" /></div>
|
|
<div><Label>Absender-E-Mail</Label><Input value={smtpFromEmail} onChange={e => setSmtpFromEmail(e.target.value)} placeholder="noreply@lageplan.ch" /></div>
|
|
</div>
|
|
<div className="flex gap-2 mt-4">
|
|
<Button onClick={handleSmtpSave} disabled={smtpLoading || !smtpHost || !smtpUser}>
|
|
{smtpLoading ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : null}
|
|
Speichern
|
|
</Button>
|
|
<Button variant="outline" onClick={handleSmtpTest} disabled={smtpLoading || !smtpHost}>
|
|
Verbindung testen
|
|
</Button>
|
|
{smtpStatus === 'connected' && <span className="flex items-center text-sm text-green-600"><CheckCircle className="w-4 h-4 mr-1" /> Verbunden</span>}
|
|
{smtpStatus === 'error' && <span className="flex items-center text-sm text-red-600"><Ban className="w-4 h-4 mr-1" /> Fehlgeschlagen</span>}
|
|
{smtpStatus === 'saved' && <span className="flex items-center text-sm text-green-600"><CheckCircle className="w-4 h-4 mr-1" /> Gespeichert</span>}
|
|
</div>
|
|
<div className="border-t mt-4 pt-4">
|
|
<Label className="text-sm font-medium">Test-E-Mail senden</Label>
|
|
<div className="flex gap-2 mt-1.5">
|
|
<Input value={smtpTestEmail} onChange={e => setSmtpTestEmail(e.target.value)} placeholder="empfaenger@example.com" className="max-w-xs" />
|
|
<Button variant="outline" onClick={handleSmtpSendTest} disabled={smtpLoading || !smtpTestEmail}>
|
|
<Send className="w-4 h-4 mr-1.5" />
|
|
Senden
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stripe Settings */}
|
|
<div className="border rounded-lg p-6 md:col-span-2">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<CreditCard className="w-5 h-5 text-primary" />
|
|
Stripe / Spenden-Konfiguration
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
Stripe API-Keys für die Spendenseite konfigurieren. Unterstützt Kreditkarte, Twint und weitere Zahlungsmethoden.
|
|
Keys findest du im <a href="https://dashboard.stripe.com/apikeys" target="_blank" rel="noopener noreferrer" className="text-primary underline">Stripe Dashboard</a>.
|
|
</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div><Label>Publishable Key (pk_...)</Label><Input value={stripePublicKey} onChange={e => setStripePublicKey(e.target.value)} placeholder="pk_live_..." /></div>
|
|
<div><Label>Secret Key (sk_...)</Label><Input type="password" value={stripeSecretKey} onChange={e => setStripeSecretKey(e.target.value)} placeholder="sk_live_..." /></div>
|
|
<div className="md:col-span-2"><Label>Webhook Secret (whsec_...) — optional</Label><Input type="password" value={stripeWebhookSecret} onChange={e => setStripeWebhookSecret(e.target.value)} placeholder="whsec_..." /></div>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
Webhook-Endpoint: <code className="bg-muted px-1.5 py-0.5 rounded text-xs">{typeof window !== 'undefined' ? window.location.origin : ''}/api/donate/webhook</code>
|
|
</p>
|
|
<div className="flex gap-2 mt-4">
|
|
<Button onClick={handleStripeSave} disabled={stripeLoading || !stripePublicKey || !stripeSecretKey}>
|
|
{stripeLoading ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : null}
|
|
Speichern
|
|
</Button>
|
|
{stripeStatus === 'saved' && <span className="flex items-center text-sm text-green-600"><CheckCircle className="w-4 h-4 mr-1" /> Gespeichert</span>}
|
|
{stripeStatus === 'error' && <span className="flex items-center text-sm text-red-600"><Ban className="w-4 h-4 mr-1" /> Fehlgeschlagen</span>}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Demo Project */}
|
|
<div className="border rounded-lg p-6 md:col-span-2">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<Map className="w-5 h-5 text-primary" />
|
|
Live-Demo auf der Startseite
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-3">
|
|
Wähle ein Projekt als Demo-Karte für die Landing Page. Besucher können die Karte sehen und zoomen, aber nichts bearbeiten.
|
|
</p>
|
|
<div className="flex gap-2 items-end">
|
|
<div className="flex-1">
|
|
<Label>Demo-Projekt</Label>
|
|
<select
|
|
value={demoProjectId}
|
|
onChange={e => setDemoProjectId(e.target.value)}
|
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
>
|
|
<option value="">— Keine Demo —</option>
|
|
{allProjects.map(p => (
|
|
<option key={p.id} value={p.id}>{p.title}{p.location ? ` (${p.location})` : ''}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<Button
|
|
onClick={async () => {
|
|
setDemoLoading(true)
|
|
setDemoStatus(null)
|
|
try {
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'save_demo_project', demoProjectId }),
|
|
})
|
|
const data = await res.json()
|
|
if (data.success) {
|
|
toast({ title: 'Demo-Projekt gespeichert' })
|
|
setDemoStatus('saved')
|
|
} else throw new Error(data.error)
|
|
} catch (error) {
|
|
toast({ title: 'Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' })
|
|
setDemoStatus('error')
|
|
} finally { setDemoLoading(false) }
|
|
}}
|
|
disabled={demoLoading}
|
|
>
|
|
{demoLoading ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : null}
|
|
Speichern
|
|
</Button>
|
|
{demoStatus === 'saved' && <span className="flex items-center text-sm text-green-600"><CheckCircle className="w-4 h-4 mr-1" /> Gespeichert</span>}
|
|
</div>
|
|
{demoProjectId && (
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
Vorschau: <a href="/demo" target="_blank" rel="noopener noreferrer" className="text-primary underline">/demo</a>
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Symbol-Grösse */}
|
|
<div className="border rounded-lg p-6">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<Settings className="w-5 h-5 text-primary" />
|
|
Standard Symbol-Grösse
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-3">
|
|
Bestimmt die Standard-Grösse neuer Symbole auf der Karte. Kleinere Werte = kleinere Symbole.
|
|
</p>
|
|
<div className="flex items-center gap-4 mb-3">
|
|
<input
|
|
type="range"
|
|
min="0.5"
|
|
max="5"
|
|
step="0.1"
|
|
value={defaultSymbolScale}
|
|
onChange={e => setDefaultSymbolScale(e.target.value)}
|
|
className="flex-1"
|
|
/>
|
|
<span className="text-lg font-bold w-16 text-center">{defaultSymbolScale}x</span>
|
|
</div>
|
|
<div className="flex items-center gap-3 text-xs text-muted-foreground mb-4">
|
|
<span>0.5x (klein)</span>
|
|
<span className="flex-1" />
|
|
<span>5x (gross)</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
disabled={symbolScaleLoading}
|
|
onClick={async () => {
|
|
setSymbolScaleLoading(true)
|
|
setSymbolScaleStatus(null)
|
|
try {
|
|
const res = await fetch('/api/admin/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'save_setting', key: 'default_symbol_scale', value: defaultSymbolScale }),
|
|
})
|
|
const data = await res.json()
|
|
if (data.success) setSymbolScaleStatus('saved')
|
|
} catch {} finally { setSymbolScaleLoading(false) }
|
|
}}
|
|
>
|
|
{symbolScaleLoading ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : null}
|
|
Speichern
|
|
</Button>
|
|
{symbolScaleStatus === 'saved' && <span className="flex items-center text-sm text-green-600"><CheckCircle className="w-4 h-4 mr-1" /> Gespeichert</span>}
|
|
</div>
|
|
</div>
|
|
|
|
{/* App Info */}
|
|
<div className="border rounded-lg p-6">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<MapPin className="w-5 h-5 text-primary" />
|
|
System-Info
|
|
</h3>
|
|
<div className="space-y-3 text-sm">
|
|
<div className="flex justify-between"><span className="text-muted-foreground">Version</span><span className="font-medium">1.0.0</span></div>
|
|
<div className="flex justify-between"><span className="text-muted-foreground">Framework</span><span className="font-medium">Next.js 14.1</span></div>
|
|
<div className="flex justify-between"><span className="text-muted-foreground">Datenbank</span><span className="font-medium">PostgreSQL 16</span></div>
|
|
<div className="flex justify-between"><span className="text-muted-foreground">Benutzer</span><span className="font-medium">{usersCount}</span></div>
|
|
<div className="flex justify-between"><span className="text-muted-foreground">Mandanten</span><span className="font-medium">{tenantsCount}</span></div>
|
|
<div className="flex justify-between"><span className="text-muted-foreground">Symbole</span><span className="font-medium">{iconsCount}</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="border rounded-lg p-6">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<Settings className="w-5 h-5 text-primary" />
|
|
Schnellaktionen
|
|
</h3>
|
|
<div className="space-y-3">
|
|
<Button variant="outline" className="w-full justify-start" onClick={() => onNavigateTab('tenants')}>
|
|
<Shield className="w-4 h-4 mr-2" />
|
|
Mandanten verwalten
|
|
</Button>
|
|
<Button variant="outline" className="w-full justify-start" onClick={() => onNavigateTab('users')}>
|
|
<UserPlus className="w-4 h-4 mr-2" />
|
|
Benutzer anlegen
|
|
</Button>
|
|
<Button variant="outline" className="w-full justify-start" asChild>
|
|
<Link href="/app">
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Zur Krokier-App
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|