"use client"; import { useState, useEffect, useCallback } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; import { Database, Search, X, ChevronDown, ChevronUp, Trash2, ExternalLink, Mail, Star, Tag, ArrowUpDown, ArrowUp, ArrowDown, CheckSquare, Square, Download, Phone, Copy, MapPin, Building2, Globe } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; // ─── Types ──────────────────────────────────────────────────────────────────── interface Lead { id: string; domain: string | null; companyName: string | null; contactName: string | null; contactTitle: string | null; email: string | null; linkedinUrl: string | null; phone: string | null; address: string | null; sourceTab: string; sourceTerm: string | null; sourceJobId: string | null; serpTitle: string | null; serpSnippet: string | null; serpRank: number | null; serpUrl: string | null; status: string; priority: string; notes: string | null; tags: string | null; country: string | null; headcount: string | null; industry: string | null; description: string | null; capturedAt: string; contactedAt: string | null; events?: Array<{ id: string; event: string; at: string }>; } interface Stats { total: number; new: number; contacted: number; withEmail: number; dailyCounts: Array<{ date: string; count: number }>; } // ─── Constants ───────────────────────────────────────────────────────────────── const STATUS_CONFIG: Record = { new: { label: "Neu", color: "text-blue-300", bg: "bg-blue-500/20 border-blue-500/30" }, in_progress: { label: "In Bearbeitung",color: "text-purple-300", bg: "bg-purple-500/20 border-purple-500/30" }, contacted: { label: "Kontaktiert", color: "text-teal-300", bg: "bg-teal-500/20 border-teal-500/30" }, qualified: { label: "Qualifiziert", color: "text-green-300", bg: "bg-green-500/20 border-green-500/30" }, not_relevant: { label: "Nicht relevant",color: "text-gray-400", bg: "bg-gray-500/20 border-gray-500/30" }, converted: { label: "Konvertiert", color: "text-amber-300", bg: "bg-amber-500/20 border-amber-500/30" }, }; const SOURCE_CONFIG: Record = { airscale: { label: "AirScale", icon: "🏢" }, linkedin: { label: "LinkedIn", icon: "💼" }, serp: { label: "SERP", icon: "🔍" }, "quick-serp": { label: "Quick SERP", icon: "⚡" }, maps: { label: "Maps", icon: "📍" }, }; const TAG_PRESETS = [ "solar", "b2b", "deutschland", "founder", "kmu", "warmkontakt", "kaltakquise", "follow-up", "interessiert", "angebot-gesendet", ]; function tagColor(tag: string): string { let hash = 0; for (let i = 0; i < tag.length; i++) hash = tag.charCodeAt(i) + ((hash << 5) - hash); const colors = [ "bg-blue-500/20 text-blue-300 border-blue-500/30", "bg-purple-500/20 text-purple-300 border-purple-500/30", "bg-green-500/20 text-green-300 border-green-500/30", "bg-amber-500/20 text-amber-300 border-amber-500/30", "bg-pink-500/20 text-pink-300 border-pink-500/30", "bg-teal-500/20 text-teal-300 border-teal-500/30", "bg-orange-500/20 text-orange-300 border-orange-500/30", ]; return colors[Math.abs(hash) % colors.length]; } // ─── Sparkline ──────────────────────────────────────────────────────────────── function Sparkline({ data, color }: { data: number[]; color: string }) { if (!data.length) return null; const max = Math.max(...data, 1); const w = 60, h = 24; const points = data.map((v, i) => `${(i / (data.length - 1)) * w},${h - (v / max) * h}`).join(" "); return ( ); } // ─── Side Panel ─────────────────────────────────────────────────────────────── function SidePanel({ lead, onClose, onUpdate, onDelete }: { lead: Lead; onClose: () => void; onUpdate: (updated: Partial) => void; onDelete: (id: string) => void; }) { const tags: string[] = JSON.parse(lead.tags || "[]"); const src = SOURCE_CONFIG[lead.sourceTab]; function copy(text: string, label: string) { navigator.clipboard.writeText(text); toast.success(`${label} kopiert`); } return (
e.stopPropagation()} > {/* Header */}

{lead.companyName || lead.domain || "Unbekannt"}

{lead.domain && ( {lead.domain} )} {lead.industry &&

{lead.industry}

}
{/* Scrollable body */}
{/* Description */} {lead.description && (

Beschreibung

{lead.description}

)} {/* Contact */} {(lead.contactName || lead.email || lead.phone || lead.address || lead.linkedinUrl) && (

Kontakt

{lead.contactName && (
{lead.contactName} {lead.contactTitle && · {lead.contactTitle}}
)} {lead.email && (
{lead.email}
)} {lead.phone && (
{lead.phone}
)} {lead.address && (
{lead.address}
)} {lead.linkedinUrl && ( )}
)} {/* Company info */} {(lead.headcount || lead.country) && (

Unternehmen

{lead.headcount && 👥 {lead.headcount} Mitarbeiter} {lead.country && 🌍 {lead.country}}
)} {/* Source */}

Quelle

{src?.icon} {src?.label || lead.sourceTab}

{lead.sourceTerm && (

Suche: "{lead.sourceTerm}"

)}

Erfasst: {new Date(lead.capturedAt).toLocaleString("de-DE")}

{/* Tags (read-only) */} {tags.length > 0 && (

Tags

{tags.map(tag => ( {tag} ))}
)}
{/* Delete */}
); } // ─── Main Page ───────────────────────────────────────────────────────────────── export default function LeadVaultPage() { // Stats const [stats, setStats] = useState(null); // Quick SERP const [serpOpen, setSerpOpen] = useState(false); const [serpQuery, setSerpQuery] = useState(""); const [serpCount, setSerpCount] = useState("25"); const serpCountry = "de"; const serpLanguage = "de"; const [serpFilter, setSerpFilter] = useState(true); const [serpRunning, setSerpRunning] = useState(false); // Filters const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const [filterStatus, setFilterStatus] = useState([]); const [filterSource, setFilterSource] = useState([]); const [filterHasEmail, setFilterHasEmail] = useState(""); const [filterContacted, setFilterContacted] = useState(false); const [filterFavorite, setFilterFavorite] = useState(false); const [filterSearchTerms, setFilterSearchTerms] = useState([]); const [availableSearchTerms, setAvailableSearchTerms] = useState([]); // Table const [leads, setLeads] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(50); const [pages, setPages] = useState(1); const [sortBy, setSortBy] = useState("capturedAt"); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); const [loading, setLoading] = useState(false); const [selected, setSelected] = useState>(new Set()); // Side panel const [panelLead, setPanelLead] = useState(null); // Export dropdown const [exportOpen, setExportOpen] = useState(false); // Bulk const [bulkTag, setBulkTag] = useState(""); // Debounce search useEffect(() => { const t = setTimeout(() => { setDebouncedSearch(search); setPage(1); }, 300); return () => clearTimeout(t); }, [search]); const loadStats = useCallback(async () => { const res = await fetch("/api/leads/stats"); if (res.ok) setStats(await res.json() as Stats); }, []); const loadLeads = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), perPage: String(perPage), search: debouncedSearch, sortBy, sortDir, }); filterStatus.forEach(s => params.append("status", s)); filterSource.forEach(s => params.append("sourceTab", s)); if (filterHasEmail) params.set("hasEmail", filterHasEmail); if (filterContacted) params.set("contacted", "yes"); if (filterFavorite) params.set("favorite", "yes"); filterSearchTerms.forEach(t => params.append("searchTerm", t)); const res = await fetch(`/api/leads?${params}`); if (res.ok) { const data = await res.json() as { leads: Lead[]; total: number; pages: number }; setLeads(data.leads); setTotal(data.total); setPages(data.pages); } } finally { setLoading(false); } }, [page, perPage, debouncedSearch, sortBy, sortDir, filterStatus, filterSource, filterHasEmail, filterContacted, filterFavorite, 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"); else { setSortBy(field); setSortDir("desc"); } setPage(1); } function toggleFilter(arr: string[], setArr: (v: string[]) => void, val: string) { setArr(arr.includes(val) ? arr.filter(x => x !== val) : [...arr, val]); setPage(1); } function clearFilters() { setSearch(""); setFilterStatus([]); setFilterSource([]); setFilterHasEmail(""); setFilterContacted(false); setFilterFavorite(false); setFilterSearchTerms([]); setPage(1); } async function updateLead(id: string, data: Partial) { // Optimistic update setLeads(prev => prev.map(l => l.id === id ? { ...l, ...data } : l)); if (panelLead?.id === id) setPanelLead(prev => prev ? { ...prev, ...data } : prev); const res = await fetch(`/api/leads/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) { toast.error("Update fehlgeschlagen"); loadLeads(); // Revert } else { loadStats(); } } async function deleteLead(id: string) { if (!confirm("Lead löschen?")) return; await fetch(`/api/leads/${id}`, { method: "DELETE" }); setLeads(prev => prev.filter(l => l.id !== id)); setTotal(prev => prev - 1); if (panelLead?.id === id) setPanelLead(null); loadStats(); toast.success("Lead gelöscht"); } async function runQuickSerp() { if (!serpQuery.trim()) return toast.error("Suchbegriff eingeben"); setSerpRunning(true); try { const res = await fetch("/api/leads/quick-serp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: serpQuery, count: Number(serpCount), country: serpCountry, language: serpLanguage, filterSocial: serpFilter }), }); 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(); loadSearchTerms(); } catch (err) { toast.error(err instanceof Error ? err.message : "Fehler"); } finally { setSerpRunning(false); } } async function bulkAction(action: "status" | "priority" | "tag" | "delete", value: string) { if (!selected.size) return; if (action === "delete" && !confirm(`${selected.size} Leads löschen?`)) return; const res = await fetch("/api/leads/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: Array.from(selected), action, value }), }); if (res.ok) { const d = await res.json() as { updated: number }; toast.success(`${d.updated} Leads aktualisiert`); setSelected(new Set()); loadLeads(); loadStats(); loadSearchTerms(); } } function exportFile(format: "csv" | "xlsx", emailOnly = false) { const params = new URLSearchParams({ search: debouncedSearch, sortBy, sortDir, format }); filterStatus.forEach(s => params.append("status", s)); filterSource.forEach(s => params.append("sourceTab", s)); if (filterHasEmail) params.set("hasEmail", filterHasEmail); if (emailOnly) params.set("emailOnly", "true"); window.open(`/api/leads/export?${params}`, "_blank"); } function SortIcon({ field }: { field: string }) { if (sortBy !== field) return ; return sortDir === "asc" ? : ; } const allSelected = leads.length > 0 && leads.every(l => selected.has(l.id)); return (
{/* Header */}
Zentrale Datenbank

🗄️ Leadspeicher

Alle Leads an einem Ort.

{/* Export dropdown — outside overflow-hidden header */} {exportOpen && ( <>
setExportOpen(false)} />
{([ ["Aktuelle Ansicht", () => exportFile("xlsx")], ["Nur mit E-Mail", () => exportFile("xlsx", true)], ] as [string, () => void][]).map(([label, fn]) => ( ))}
)} {/* Stats */} {stats && (
{[ { label: "Leads gesamt", value: stats.total, color: "#a78bfa" }, { label: "Neu / Nicht kontaktiert", value: stats.new, color: "#60a5fa" }, { label: "Kontaktiert / In Bearbeitung", value: stats.contacted, color: "#2dd4bf" }, { label: "Mit verifizierter E-Mail", value: stats.withEmail, color: "#34d399" }, ].map(({ label, value, color }) => (

{label}

{value.toLocaleString()}

d.count)} color={color} />
))}
)} {/* Quick SERP */} {serpOpen && (
setSerpQuery(e.target.value)} onKeyDown={e => e.key === "Enter" && runQuickSerp()} className="bg-[#0d0d18] border-[#2e2e3e] text-white placeholder:text-gray-600 focus:border-purple-500" />
)}
{/* Filter Bar */}
{/* Search */}
setSearch(e.target.value)} placeholder="Domain, Firma, Name, E-Mail suchen..." className="w-full bg-[#0d0d18] border border-[#2e2e3e] rounded-lg pl-8 pr-3 py-1.5 text-sm text-white outline-none focus:border-purple-500" />
{/* Has Email toggle */}
{[["", "Alle"], ["yes", "Mit E-Mail"], ["no", "Ohne E-Mail"]].map(([v, l]) => ( ))}
{/* Kontaktiert toggle */} {/* Favoriten toggle */} {/* Clear + count */}
{(search || filterHasEmail || filterContacted || filterFavorite || filterSearchTerms.length) && ( )} {total.toLocaleString()} Leads
{/* Bulk Actions */} {selected.size > 0 && (
{selected.size} ausgewählt
setBulkTag(e.target.value)} placeholder="Tag hinzufügen..." className="bg-[#1a1a28] border border-[#2e2e3e] text-gray-300 text-xs rounded px-2 py-1 w-32 outline-none" />
)} {/* Table */}
{[ ["companyName", "Unternehmen"], ["contactName", "Kontakt"], ["phone", "Telefon"], ["email", "E-Mail"], ["sourceTerm", "Suchbegriff"], ["capturedAt", "Erfasst"], ].map(([field, label]) => ( ))} {loading && !leads.length ? ( ) : leads.length === 0 ? ( ) : leads.map((lead, i) => { const tags: string[] = JSON.parse(lead.tags || "[]"); const isSelected = selected.has(lead.id); const cfg = STATUS_CONFIG[lead.status] || STATUS_CONFIG.new; const src = SOURCE_CONFIG[lead.sourceTab]; const isFavorite = tags.includes("favorit"); const isContacted = tags.includes("kontaktiert"); return ( setPanelLead(lead)} className={`border-b border-[#1a1a28] cursor-pointer transition-colors hover:bg-[#1a1a28] ${ isSelected ? "bg-[#1a1a2e]" : i % 2 === 0 ? "bg-[#111118]" : "bg-[#0f0f16]" }`} > ); })}
Tags Aktionen
Lädt...

{(search || filterSource.length || filterContacted || filterFavorite) ? "Keine Leads für diese Filter." : "Noch keine Leads. Pipeline ausführen oder Quick SERP nutzen."}

{search && ( )}
e.stopPropagation()}>

{lead.companyName || "–"}

{lead.domain}

{(lead.description || lead.industry) && (

{lead.industry ? `${lead.industry}${lead.description ? " · " + lead.description : ""}` : lead.description}

)}

{lead.contactName || "–"}

{lead.contactTitle &&

{lead.contactTitle}

}
{lead.phone ? ( e.stopPropagation()} className="text-xs text-gray-300 hover:text-white whitespace-nowrap"> {lead.phone} ) : lead.address ? (

{lead.address}

) : ( )} {lead.phone && lead.address && (

{lead.address}

)}
e.stopPropagation()}> {lead.email ? ( ) : ( )} {lead.sourceTerm ? ( ) : ( )} {new Date(lead.capturedAt).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })}
{tags.slice(0, 2).map(tag => ( {tag} ))} {tags.length > 2 && +{tags.length - 2}}
e.stopPropagation()}>
{lead.domain && ( )}
{/* Pagination */} {pages > 1 && (
Zeilen pro Seite:
{Array.from({ length: Math.min(7, pages) }, (_, i) => { let p = i + 1; if (pages > 7) { if (page <= 4) p = i + 1; else if (page >= pages - 3) p = pages - 6 + i; else p = page - 3 + i; } return ( ); })}
Seite {page} von {pages}
)}
{/* Side Panel */} {panelLead && ( setPanelLead(null)} onUpdate={updated => { setPanelLead(prev => prev ? { ...prev, ...updated } : prev); setLeads(prev => prev.map(l => l.id === panelLead.id ? { ...l, ...updated } : l)); }} onDelete={id => { deleteLead(id); }} /> )}
); }