e70ae79d-5898-4c55-8ceb-d2c701ce08b4
This commit is contained in:
1 parent
d226dabba3
commit
5855f699f3
53 files changed
+1911
-44
No files matched your search
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
Receipt,
|
||||
Settings2,
|
||||
RotateCcw,
|
||||
ScrollText,
|
||||
Search as SearchIcon,
|
||||
ArrowRight,
|
||||
} from 'lucide-react'
|
||||
import { useAos, type ViewId } from '@/store/aos'
|
||||
import {
|
||||
@@ -31,14 +34,26 @@ const VIEWS: { id: ViewId; label: string; icon: typeof LayoutDashboard; hint: st
|
||||
{ id: 'projects', label: 'Projekte', icon: FolderKanban, hint: 'Aufgaben & Zeit' },
|
||||
{ id: 'licenses', label: 'Lizenzen', icon: KeyRound, hint: 'Ausgestellt & Erneuerungen' },
|
||||
{ id: 'finance', label: 'Finanzen', icon: Receipt, hint: 'Rechnungen & E-Rechnung' },
|
||||
{ id: 'audit', label: 'Audit-Protokoll', icon: ScrollText, hint: 'Hash-Kette & GoBD' },
|
||||
{ id: 'settings', label: 'Erweiterungen', icon: Settings2, hint: 'Module & Compliance' },
|
||||
]
|
||||
|
||||
interface SearchResult {
|
||||
group: string
|
||||
view: ViewId
|
||||
id: string
|
||||
primary: string
|
||||
secondary?: string
|
||||
tertiary?: string
|
||||
}
|
||||
|
||||
export function CommandMenu() {
|
||||
const { commandOpen, setCommandOpen, setView } = useAos()
|
||||
const { commandOpen, setCommandOpen, setView, setSelectedCompany } = useAos()
|
||||
const router = useRouter()
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<SearchResult[]>([])
|
||||
const [searching, setSearching] = useState(false)
|
||||
|
||||
// keyboard shortcut cmd+k
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
||||
@@ -50,11 +65,47 @@ export function CommandMenu() {
|
||||
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…')
|
||||
@@ -70,9 +121,29 @@ export function CommandMenu() {
|
||||
|
||||
return (
|
||||
<CommandDialog open={commandOpen} onOpenChange={setCommandOpen}>
|
||||
<CommandInput placeholder="Modul, Aktion oder Seite suchen…" />
|
||||
<CommandInput placeholder="Suche Firmen, Deals, Leads, Projekte, Rechnungen…" value={query} onValueChange={setQuery} />
|
||||
<CommandList>
|
||||
<CommandEmpty>Keine Treffer.</CommandEmpty>
|
||||
<CommandEmpty>{searching ? 'Suche läuft…' : query.length > 1 ? 'Keine Treffer.' : 'Tippe mindestens 2 Zeichen für globale Suche.'}</CommandEmpty>
|
||||
|
||||
{results.length > 0 && (
|
||||
<>
|
||||
<CommandGroup heading={`Suchergebnisse (${results.length})`}>
|
||||
{results.slice(0, 12).map((r) => (
|
||||
<CommandItem key={`${r.group}-${r.id}`} value={`${r.primary} ${r.secondary ?? ''} ${r.tertiary ?? ''}`} onSelect={() => goResult(r)}>
|
||||
<SearchIcon className="size-4 text-muted-foreground" />
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className="truncate">{r.primary}</span>
|
||||
{r.secondary && <span className="truncate text-xs text-muted-foreground">· {r.secondary}</span>}
|
||||
</div>
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">{r.group}</span>
|
||||
<ArrowRight className="size-3 shrink-0 text-muted-foreground" />
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<CommandGroup heading="Navigation">
|
||||
{VIEWS.map((v) => {
|
||||
const Icon = v.icon
|
||||
|
||||
Reference in new issue
Block a user