From 1dafb7bbe7d4661f566d92c7f89461da9149e100 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 20:55:52 +0000 Subject: [PATCH] feat(CC-056): add Global Search page with debounced input and grouped results --- src/app/dashboard/search/page.tsx | 183 ++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 src/app/dashboard/search/page.tsx diff --git a/src/app/dashboard/search/page.tsx b/src/app/dashboard/search/page.tsx new file mode 100644 index 0000000..a508f9b --- /dev/null +++ b/src/app/dashboard/search/page.tsx @@ -0,0 +1,183 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import Link from 'next/link'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface SearchItem { + type: string; + id: string; + title: string; + subtitle: string; + route: string; +} + +interface SearchGroup { + type: string; + label: string; + items: SearchItem[]; +} + +interface SearchResponse { + q: string; + totalCount: number; + groups: SearchGroup[]; +} + +const TYPE_META: Record = { + organization: { emoji: '🏢', color: 'bg-blue-500/10 text-blue-700 dark:text-blue-300' }, + task: { emoji: '✅', color: 'bg-amber-500/10 text-amber-700 dark:text-amber-300' }, + goal: { emoji: '🎯', color: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' }, + decision: { emoji: '⚖️', color: 'bg-purple-500/10 text-purple-700 dark:text-purple-300' }, + research: { emoji: '📋', color: 'bg-sky-500/10 text-sky-700 dark:text-sky-300' }, + segment: { emoji: '📊', color: 'bg-rose-500/10 text-rose-700 dark:text-rose-300' }, +}; + +export default function SearchPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + + const [query, setQuery] = useState(''); + const [debouncedQ, setDebouncedQ] = useState(''); + const [results, setResults] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const t = setTimeout(() => setDebouncedQ(query), 300); + return () => clearTimeout(t); + }, [query]); + + const doSearch = useCallback(async (q: string) => { + if (!q || q.length < 2 || !tenantId) { + setResults(null); + return; + } + setLoading(true); + setError(null); + try { + const data = await apiFetch(`/v1/search?q=${encodeURIComponent(q)}`, { tenantId }); + setResults(data); + } catch (e) { + setError(e instanceof Error ? e.message : 'Eroare la căutare'); + setResults(null); + } finally { + setLoading(false); + } + }, [tenantId]); + + useEffect(() => { + doSearch(debouncedQ); + }, [debouncedQ, doSearch]); + + const totalCount = results?.totalCount ?? 0; + + return ( +
+ {/* Header */} +
+

Căutare globală

+

+ Caută în organizații, taskuri, obiective, decizii, research și segmente +

+
+ + {/* Search input */} +
+
+ 🔍 +
+ setQuery(e.target.value)} + placeholder="Caută orice — organizație, task, decizie…" + className="w-full rounded-xl border bg-background pl-10 pr-4 py-3 text-sm + shadow-sm focus:outline-none focus:ring-2 focus:ring-ring" + /> + {loading && ( +
+ Se caută… +
+ )} +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Empty query */} + {!query && ( +
+

🔍

+

Introdu cel puțin 2 caractere pentru a căuta

+
+ {['GmbH', 'angajat', 'contract', 'decizie', 'buget'].map((s) => ( + + ))} +
+
+ )} + + {/* No results */} + {query.length >= 2 && !loading && results && totalCount === 0 && ( +
+

+ Niciun rezultat pentru "{query}" +

+
+ )} + + {/* Results */} + {results && totalCount > 0 && ( +
+

+ {totalCount} rezultat{totalCount !== 1 ? 'e' : ''} pentru "{results.q}" +

+ + {results.groups.map((group) => ( +
+

+ {group.label} +

+
+ {group.items.map((item) => { + const meta = TYPE_META[item.type] ?? { emoji: '•', color: 'bg-muted/50 text-ink-faint' }; + return ( + + + {meta.emoji} + +
+

{item.title}

+ {item.subtitle && ( +

{item.subtitle}

+ )} +
+ + + ); + })} +
+
+ ))} +
+ )} +
+ ); +}