"use client"; import { useState, useEffect } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { ProgressCard } from "@/components/shared/ProgressCard"; import { ResultsTable, type ResultRow } from "@/components/shared/ResultsTable"; import { ExportButtons } from "@/components/shared/ExportButtons"; import { EmptyState } from "@/components/shared/EmptyState"; import { toast } from "sonner"; import { Linkedin, ChevronRight, AlertTriangle, CheckCircle2, XCircle, ChevronDown, ChevronUp, Info } from "lucide-react"; import { useAppStore } from "@/lib/store"; import type { DecisionMakerCategory } from "@/lib/services/anymailfinder"; import type { ExportRow } from "@/lib/utils/csv"; const CATEGORY_OPTIONS: { value: DecisionMakerCategory; label: string; recommended?: boolean }[] = [ { value: "ceo", label: "CEO / Owner / President / Founder", recommended: true }, { value: "operations", label: "COO" }, { value: "engineering", label: "CTO" }, { value: "marketing", label: "CMO" }, { value: "finance", label: "CFO" }, { value: "sales", label: "Vertriebsleiter" }, ]; type Stage = "idle" | "scraping" | "scraped" | "enriching" | "done" | "failed"; export default function LinkedInPage() { const [salesNavUrl, setSalesNavUrl] = useState(""); const [maxResults, setMaxResults] = useState(100); const [vayneConfigured, setVayneConfigured] = useState(null); const [guideOpen, setGuideOpen] = useState(false); const [stage, setStage] = useState("idle"); const [scrapeJobId, setScrapeJobId] = useState(null); const [enrichJobId, setEnrichJobId] = useState(null); const [scrapeProgress, setScrapeProgress] = useState({ current: 0, total: 0 }); const [enrichProgress, setEnrichProgress] = useState({ current: 0, total: 0 }); const [results, setResults] = useState([]); const [selectedIds, setSelectedIds] = useState([]); const [category, setCategory] = useState("ceo"); const { addJob, updateJob, removeJob } = useAppStore(); useEffect(() => { fetch("/api/credentials") .then(r => r.json()) .then((d: Record) => setVayneConfigured(d.vayne)) .catch(() => setVayneConfigured(false)); }, []); const urlValid = salesNavUrl.includes("linkedin.com/sales"); const startScrape = async () => { if (!urlValid) return toast.error("Bitte gültige Sales Navigator URL einfügen"); if (!vayneConfigured) return toast.error("Bitte zuerst Vayne API-Token in den Einstellungen konfigurieren"); setStage("scraping"); setScrapeProgress({ current: 0, total: maxResults }); setResults([]); try { const res = await fetch("/api/jobs/vayne-scrape", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ salesNavUrl, maxResults }), }); const data = await res.json() as { jobId?: string; error?: string }; if (!res.ok || !data.jobId) throw new Error(data.error || "Failed"); setScrapeJobId(data.jobId); addJob({ id: data.jobId, type: "linkedin-scrape", status: "running", progress: 0, total: maxResults }); pollScrape(data.jobId); } catch (err) { toast.error(err instanceof Error ? err.message : "Scraping konnte nicht gestartet werden"); setStage("failed"); } }; const pollScrape = (id: string) => { const interval = setInterval(async () => { try { const res = await fetch(`/api/jobs/${id}/status`); const data = await res.json() as { status: string; totalLeads: number; results: ResultRow[]; }; setScrapeProgress({ current: data.totalLeads, total: maxResults }); if (data.results?.length) setResults(data.results); updateJob(id, { status: data.status, progress: data.totalLeads }); if (data.status === "complete" || data.status === "failed") { clearInterval(interval); removeJob(id); if (data.status === "complete") { setStage("scraped"); setResults(data.results || []); setSelectedIds(data.results?.map(r => r.id) || []); toast.success(`${data.totalLeads} Profile von LinkedIn gescrapt`); } else { setStage("failed"); toast.error("Scraping fehlgeschlagen. Bitte Vayne-Token in den Einstellungen prüfen."); } } } catch { clearInterval(interval); setStage("failed"); } }, 2000); }; const startEnrich = async () => { if (!selectedIds.length) return toast.error("Mindestens ein Profil zum Anreichern auswählen"); setStage("enriching"); setEnrichProgress({ current: 0, total: selectedIds.length }); try { const res = await fetch("/api/jobs/linkedin-enrich", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jobId: scrapeJobId, resultIds: selectedIds, categories: [category] }), }); const data = await res.json() as { jobId?: string; error?: string }; if (!res.ok || !data.jobId) throw new Error(data.error || "Failed"); setEnrichJobId(data.jobId); addJob({ id: data.jobId, type: "linkedin-enrich", status: "running", progress: 0, total: selectedIds.length }); pollEnrich(data.jobId); } catch (err) { toast.error(err instanceof Error ? err.message : "Anreicherung konnte nicht gestartet werden"); setStage("scraped"); } }; const pollEnrich = (id: string) => { const interval = setInterval(async () => { try { const res = await fetch(`/api/jobs/${id}/status`); const data = await res.json() as { status: string; totalLeads: number; emailsFound: number; }; setEnrichProgress({ current: data.emailsFound, total: data.totalLeads }); updateJob(id, { status: data.status, progress: data.emailsFound }); if (data.status === "complete" || data.status === "failed") { clearInterval(interval); removeJob(id); // Refresh results from scrape job const jobRes = await fetch(`/api/jobs/${scrapeJobId}/status`); const jobData = await jobRes.json() as { results: ResultRow[] }; setResults(jobData.results || []); if (data.status === "complete") { setStage("done"); toast.success(`${data.emailsFound} E-Mails gefunden`); } else { setStage("scraped"); toast.error("Anreicherung fehlgeschlagen"); } } } catch { clearInterval(interval); } }, 2000); }; const exportRows: ExportRow[] = results.map(r => ({ company_name: r.companyName, domain: r.domain, contact_name: r.contactName, contact_title: r.contactTitle, email: r.email, confidence_score: r.confidence !== undefined ? Math.round(r.confidence * 100) : undefined, source_tab: "linkedin", job_id: scrapeJobId || "", found_at: new Date().toISOString(), })); const emailsFound = results.filter(r => r.email).length; return (
{/* Header */}
Tab 2 LinkedIn Sales Navigator

LinkedIn → E-Mail Pipeline

Scrape Sales Navigator-Profile über Vayne und reichere sie mit Anymailfinder an.

{/* Filter Guide */} {guideOpen && (

Zielpositionen

{["Founder", "Co-Founder", "CEO", "CTO", "COO", "CMO", "Owner", "Owner Operator", "President", "Principal", "Partner", "CXO", "Geschäftsführer", "Inhaber"].map(k => ( {k} ))}

Mitarbeiterzahl

{["1–10", "11–50", "51–200"].map(k => ( {k} ))}

Land

Deutschland

Schlüsselwörter (optional)

{["Solaranlage", "Photovoltaik", "Solar", "Handwerker", "Installateur"].map(k => ( {k} ))}
)}
{/* Step 1: Input */}

1 Sales Navigator URL

{/* Vayne status */}
{vayneConfigured === null ? "Vayne-Konfiguration wird geprüft..." : vayneConfigured ? ( <> Vayne API-Token konfiguriert ) : ( <> Vayne-Token nicht konfiguriert — zu den Einstellungen )}