"use client"; import { useState, useEffect, useCallback, useRef } 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, FileText, ArrowUpDown, ArrowUp, ArrowDown, CheckSquare, Square, Download, Plus, RefreshCw, Phone } 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; sourceTab: string; sourceTerm: string | null; sourceJobId: string | null; serpTitle: string | null; serpSnippet: string | null; serpRank: number | null; serpUrl: string | null; emailConfidence: number | null; status: string; priority: string; notes: string | null; tags: string | null; country: string | null; headcount: string | null; industry: 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 PRIORITY_CONFIG: Record = { high: { label: "Hoch", icon: "↑", color: "text-red-400" }, normal: { label: "Normal", icon: "→", color: "text-gray-400" }, low: { label: "Niedrig",icon: "↓", color: "text-blue-400" }, }; 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]; } function relativeTime(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return "gerade eben"; if (mins < 60) return `vor ${mins} Min.`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `vor ${hrs} Std.`; const days = Math.floor(hrs / 24); if (days < 30) return `vor ${days} Tagen`; return new Date(dateStr).toLocaleDateString("de-DE"); } // ─── 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 ( ); } // ─── Status Badge ───────────────────────────────────────────────────────────── function StatusBadge({ status, onChange }: { status: string; onChange: (s: string) => void }) { const [open, setOpen] = useState(false); const cfg = STATUS_CONFIG[status] || STATUS_CONFIG.new; return (
{open && (
{Object.entries(STATUS_CONFIG).map(([key, c]) => ( ))}
)}
); } // ─── Side Panel ─────────────────────────────────────────────────────────────── function SidePanel({ lead, onClose, onUpdate }: { lead: Lead; onClose: () => void; onUpdate: (updated: Partial) => void; }) { const [notes, setNotes] = useState(lead.notes || ""); const [tagInput, setTagInput] = useState(""); const [fullLead, setFullLead] = useState(lead); const notesTimer = useRef | null>(null); useEffect(() => { // Load full lead with events fetch(`/api/leads/${lead.id}`).then(r => r.json()).then(setFullLead).catch(() => {}); }, [lead.id]); const tags: string[] = JSON.parse(fullLead.tags || "[]"); async function patch(data: Partial) { const res = await fetch(`/api/leads/${lead.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (res.ok) { const updated = await res.json() as Lead; setFullLead(prev => ({ ...prev, ...updated })); onUpdate(updated); } } function handleNotesChange(v: string) { setNotes(v); if (notesTimer.current) clearTimeout(notesTimer.current); notesTimer.current = setTimeout(() => patch({ notes: v }), 500); } function addTag(tag: string) { const trimmed = tag.trim().toLowerCase(); if (!trimmed || tags.includes(trimmed)) return; const newTags = [...tags, trimmed]; patch({ tags: JSON.stringify(newTags) }); setTagInput(""); } function removeTag(tag: string) { patch({ tags: JSON.stringify(tags.filter(t => t !== tag)) }); } const conf = fullLead.emailConfidence; const confColor = conf == null ? "text-gray-500" : conf >= 0.8 ? "text-green-400" : conf >= 0.5 ? "text-yellow-400" : "text-red-400"; return (
e.stopPropagation()} > {/* Header */}

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

{fullLead.domain}

{/* Status + Priority */}
patch({ status: s })} />
{Object.entries(PRIORITY_CONFIG).map(([key, p]) => ( ))}
{/* Contact Info */}

Kontakt

{fullLead.contactName &&

{fullLead.contactName}{fullLead.contactTitle && · {fullLead.contactTitle}}

} {fullLead.email && (
{fullLead.email} {conf != null && {Math.round(conf * 100)}%}
)} {fullLead.phone && (
{fullLead.phone}
)} {fullLead.linkedinUrl && ( LinkedIn )}
{/* Source Info */}

Quelle

{SOURCE_CONFIG[fullLead.sourceTab]?.icon} {SOURCE_CONFIG[fullLead.sourceTab]?.label || fullLead.sourceTab}

{fullLead.sourceTerm &&

Suche: {fullLead.sourceTerm}

}

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

{fullLead.serpRank &&

SERP Rang: #{fullLead.serpRank}

} {fullLead.serpUrl && URL öffnen}
{/* Tags */}

Tags

{tags.map(tag => ( {tag} ))}
{TAG_PRESETS.filter(t => !tags.includes(t)).slice(0, 5).map(t => ( ))}
setTagInput(e.target.value)} onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); addTag(tagInput); } }} placeholder="Tag hinzufügen..." className="flex-1 bg-[#0d0d18] border border-[#2e2e3e] rounded px-2 py-1 text-xs text-white outline-none focus:border-purple-500" />
{/* Notes */}

Notizen