'use client' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { LayoutDashboard, Users, Radar, FolderKanban, KeyRound, Receipt, Settings2, RotateCcw, ScrollText, Search as SearchIcon, ArrowRight, FileSignature, Wallet, FileText, LifeBuoy, BarChart3, Rocket, BookOpen, Webhook, HelpCircle, SlidersHorizontal, CalendarDays, } from 'lucide-react' import { useAos, type ViewId } from '@/store/aos' import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from '@/components/ui/command' import { toast } from 'sonner' const VIEWS: { id: ViewId; label: string; icon: typeof LayoutDashboard; hint: string }[] = [ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard, hint: 'Übersicht & KPIs' }, { id: 'calendar', label: 'Fristen', icon: CalendarDays, hint: 'Kalender & Deadlines' }, { id: 'reporting', label: 'Reporting', icon: BarChart3, hint: 'Pipeline, Auslastung, MRR' }, { id: 'crm', label: 'CRM', icon: Users, hint: 'Firmen & Pipeline' }, { id: 'leads', label: 'Leads Finder', icon: Radar, hint: 'Review-Queue' }, { id: 'estimates', label: 'Angebote', icon: FileText, hint: 'Quotes & Conversion' }, { id: 'projects', label: 'Projekte', icon: FolderKanban, hint: 'Aufgaben & Zeit' }, { id: 'support', label: 'Support', icon: LifeBuoy, hint: 'Tickets & SLA' }, { id: 'licenses', label: 'Lizenzen', icon: KeyRound, hint: 'Ausgestellt & Erneuerungen' }, { id: 'contracts', label: 'Verträge', icon: FileSignature, hint: 'Werkvertrag, AVV, NDA' }, { id: 'expenses', label: 'Ausgaben', icon: Wallet, hint: 'Belastbar & DATEV' }, { id: 'finance', label: 'Finanzen', icon: Receipt, hint: 'Rechnungen & E-Rechnung' }, { id: 'accounting', label: 'Buchhaltung', icon: BookOpen, hint: 'USt-Voranmeldung, EÜR, Bank' }, { id: 'audit', label: 'Audit-Protokoll', icon: ScrollText, hint: 'Hash-Kette & GoBD' }, { id: 'kb', label: 'Wissensdatenbank', icon: HelpCircle, hint: 'Dokumente & Vorlagen' }, { id: 'api', label: 'API & Webhooks', icon: Webhook, hint: 'Tokens, Scopes, HMAC' }, { id: 'settings', label: 'Erweiterungen', icon: Settings2, hint: 'Module & Compliance' }, { id: 'settings_global', label: 'Einstellungen', icon: SlidersHorizontal, hint: 'Stammdaten, Steuern, i18n' }, { id: 'setup', label: 'Setup-Wizard', icon: Rocket, hint: '≤ 15 Min Installation' }, ] interface SearchResult { group: string view: ViewId id: string primary: string secondary?: string tertiary?: string } export function CommandMenu() { const { commandOpen, setCommandOpen, setView, setSelectedCompany } = useAos() const router = useRouter() const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [searching, setSearching] = useState(false) useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault() setCommandOpen(!commandOpen) } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, [commandOpen, setCommandOpen]) // debounced global search useEffect(() => { if (!commandOpen) { setQuery('') setResults([]) return } const q = query.trim() if (q.length < 2) { setResults([]) return } setSearching(true) const t = setTimeout(async () => { try { const r = await fetch(`/api/aos/search?q=${encodeURIComponent(q)}`) if (!r.ok) throw new Error('search failed') const data = (await r.json()) as { results: SearchResult[] } setResults(data.results) } catch { setResults([]) } finally { setSearching(false) } }, 200) return () => clearTimeout(t) }, [query, commandOpen]) const go = (v: ViewId) => { setView(v) setCommandOpen(false) } const goResult = (r: SearchResult) => { if (r.view === 'crm' && r.group === 'Firmen') { setSelectedCompany(r.id) } setView(r.view) setCommandOpen(false) } const reseed = async () => { setCommandOpen(false) const t = toast.loading('Datenbank wird zurückgesetzt…') try { const r = await fetch('/api/aos/seed', { method: 'POST' }) if (!r.ok) throw new Error('reseed failed') toast.success('Datenbank neu befüllt', { id: t }) setTimeout(() => router.refresh(), 400) } catch { toast.error('Reseed fehlgeschlagen', { id: t }) } } return ( {searching ? 'Suche läuft…' : query.length > 1 ? 'Keine Treffer.' : 'Tippe mindestens 2 Zeichen für globale Suche.'} {results.length > 0 && ( <> {results.slice(0, 12).map((r) => ( goResult(r)}>
{r.primary} {r.secondary && · {r.secondary}}
{r.group}
))}
)} {VIEWS.map((v) => { const Icon = v.icon return ( go(v.id)}> {v.label} {v.hint} ) })} Demo-Daten zurücksetzen
) }