From 82c4244233bd1d8322ad197036fc990b69021d64 Mon Sep 17 00:00:00 2001 From: Timo Uttenweiler Date: Fri, 20 Mar 2026 17:53:40 +0100 Subject: [PATCH] feat: Suchbegriff-Spalte + Filter-Chips im LeadVault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/leads/search-terms: distinct Suchbegriffe aus DB - Filter-Bar: Suchbegriff-Chips (amber), klickbar zum Filtern - Tabelle: Suchbegriff-Spalte mit Chip — Klick filtert direkt - Mehrere Suchbegriffe gleichzeitig filterbar (OR-Logik) Co-Authored-By: Claude Sonnet 4.6 --- app/api/leads/route.ts | 6 +-- app/api/leads/search-terms/route.ts | 23 +++++++++++ app/leadvault/page.tsx | 59 +++++++++++++++++++++++++---- 3 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 app/api/leads/search-terms/route.ts diff --git a/app/api/leads/route.ts b/app/api/leads/route.ts index c6358c4..2c1c4dc 100644 --- a/app/api/leads/route.ts +++ b/app/api/leads/route.ts @@ -19,7 +19,7 @@ export async function GET(req: NextRequest) { const sourceTabs = searchParams.getAll("sourceTab"); const priorities = searchParams.getAll("priority"); const tags = searchParams.getAll("tags"); - const searchTerm = searchParams.get("searchTerm") || ""; + const searchTerms = searchParams.getAll("searchTerm"); const where: Prisma.LeadWhereInput = {}; @@ -46,8 +46,8 @@ export async function GET(req: NextRequest) { if (capturedTo) where.capturedAt.lte = new Date(capturedTo); } - if (searchTerm) { - where.sourceTerm = { contains: searchTerm }; + if (searchTerms.length > 0) { + where.sourceTerm = { in: searchTerms }; } if (tags.length > 0) { diff --git a/app/api/leads/search-terms/route.ts b/app/api/leads/search-terms/route.ts new file mode 100644 index 0000000..4fbd0b8 --- /dev/null +++ b/app/api/leads/search-terms/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET() { + try { + const results = await prisma.lead.findMany({ + where: { sourceTerm: { not: null } }, + select: { sourceTerm: true }, + distinct: ["sourceTerm"], + orderBy: { sourceTerm: "asc" }, + }); + + const terms = results + .map(r => r.sourceTerm!) + .filter(Boolean) + .sort(); + + return NextResponse.json(terms); + } catch (err) { + console.error("GET /api/leads/search-terms error:", err); + return NextResponse.json([], { status: 500 }); + } +} diff --git a/app/leadvault/page.tsx b/app/leadvault/page.tsx index 87e9386..266589c 100644 --- a/app/leadvault/page.tsx +++ b/app/leadvault/page.tsx @@ -387,7 +387,8 @@ export default function LeadVaultPage() { const [filterSource, setFilterSource] = useState([]); const [filterPriority, setFilterPriority] = useState([]); const [filterHasEmail, setFilterHasEmail] = useState(""); - const [filterSearchTerm, setFilterSearchTerm] = useState(""); + const [filterSearchTerms, setFilterSearchTerms] = useState([]); + const [availableSearchTerms, setAvailableSearchTerms] = useState([]); // Table const [leads, setLeads] = useState([]); @@ -429,7 +430,7 @@ export default function LeadVaultPage() { filterSource.forEach(s => params.append("sourceTab", s)); filterPriority.forEach(p => params.append("priority", p)); if (filterHasEmail) params.set("hasEmail", filterHasEmail); - if (filterSearchTerm) params.set("searchTerm", filterSearchTerm); + filterSearchTerms.forEach(t => params.append("searchTerm", t)); const res = await fetch(`/api/leads?${params}`); if (res.ok) { @@ -441,10 +442,16 @@ export default function LeadVaultPage() { } finally { setLoading(false); } - }, [page, perPage, debouncedSearch, sortBy, sortDir, filterStatus, filterSource, filterPriority, filterHasEmail, filterSearchTerm]); + }, [page, perPage, debouncedSearch, sortBy, sortDir, filterStatus, filterSource, filterPriority, filterHasEmail, filterSearchTerms]); + + const loadSearchTerms = useCallback(async () => { + const res = await fetch("/api/leads/search-terms"); + if (res.ok) setAvailableSearchTerms(await res.json() as string[]); + }, []); useEffect(() => { loadStats(); const t = setInterval(loadStats, 30000); return () => clearInterval(t); }, [loadStats]); useEffect(() => { loadLeads(); }, [loadLeads]); + useEffect(() => { loadSearchTerms(); }, [loadSearchTerms]); function toggleSort(field: string) { if (sortBy === field) setSortDir(d => d === "asc" ? "desc" : "asc"); @@ -459,7 +466,7 @@ export default function LeadVaultPage() { function clearFilters() { setSearch(""); setFilterStatus([]); setFilterSource([]); - setFilterPriority([]); setFilterHasEmail(""); setFilterSearchTerm(""); + setFilterPriority([]); setFilterHasEmail(""); setFilterSearchTerms([]); setPage(1); } @@ -503,7 +510,7 @@ export default function LeadVaultPage() { const data = await res.json() as { added?: number; updated?: number; skipped?: number; error?: string }; if (!res.ok) throw new Error(data.error || "Fehler"); toast.success(`${data.added} Leads hinzugefügt, ${data.skipped} übersprungen`); - loadLeads(); loadStats(); + loadLeads(); loadStats(); loadSearchTerms(); } catch (err) { toast.error(err instanceof Error ? err.message : "Fehler"); } finally { @@ -523,7 +530,7 @@ export default function LeadVaultPage() { const d = await res.json() as { updated: number }; toast.success(`${d.updated} Leads aktualisiert`); setSelected(new Set()); - loadLeads(); loadStats(); + loadLeads(); loadStats(); loadSearchTerms(); } } @@ -700,7 +707,7 @@ export default function LeadVaultPage() { {/* Clear + count */}
- {(search || filterStatus.length || filterSource.length || filterPriority.length || filterHasEmail || filterSearchTerm) && ( + {(search || filterStatus.length || filterSource.length || filterPriority.length || filterHasEmail || filterSearchTerms.length) && ( @@ -743,6 +750,26 @@ export default function LeadVaultPage() { ))}
+ + {/* Search term filter chips */} + {availableSearchTerms.length > 0 && ( + <> +
+
+ Suchbegriff + {availableSearchTerms.map(term => ( + + ))} +
+ + )}
@@ -803,6 +830,7 @@ export default function LeadVaultPage() { ["contactName", "Kontakt"], ["phone", "Telefon"], ["email", "E-Mail"], + ["sourceTerm", "Suchbegriff"], ["sourceTab", "Quelle"], ["capturedAt", "Erfasst"], ].map(([field, label]) => ( @@ -912,6 +940,23 @@ export default function LeadVaultPage() { )} + + {lead.sourceTerm ? ( + + ) : ( + + )} + {src?.icon} {src?.label || lead.sourceTab}