"use client"; import { useState } 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 { Checkbox } from "@/components/ui/checkbox"; 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 { MapPin, ChevronRight, Plus, X, Info } from "lucide-react"; import { useAppStore } from "@/lib/store"; import type { DecisionMakerCategory } from "@/lib/services/anymailfinder"; import type { ExportRow } from "@/lib/utils/csv"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 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" }, ]; const RESULTS_OPTIONS = [ { value: "20", label: "20 per query" }, { value: "40", label: "40 per query" }, { value: "60", label: "60 per query (max)" }, ]; const KEYWORD_PRESETS = [ "Solaranlage Installateur", "Dachdecker", "Elektriker", "Heizungsbauer", "Sanitär", "Steuerberater", "Rechtsanwalt", "Zahnarzt", "Physiotherapeut", "Immobilienmakler", ]; const GERMAN_REGIONS = [ "Bayern", "Baden-Württemberg", "Nordrhein-Westfalen", "Hessen", "Niedersachsen", "Sachsen", "Rheinland-Pfalz", "Brandenburg", "Berlin", "Hamburg", "München", "Frankfurt", "Stuttgart", "Düsseldorf", "Köln", "Leipzig", "Dresden", ]; type Stage = "idle" | "running" | "done" | "failed"; export default function MapsPage() { const [keywords, setKeywords] = useState(["Solaranlage Installateur"]); const [keywordInput, setKeywordInput] = useState(""); const [regions, setRegions] = useState(["Bayern", "Baden-Württemberg"]); const [regionInput, setRegionInput] = useState(""); const [maxResults, setMaxResults] = useState("60"); const [enrichEmails, setEnrichEmails] = useState(true); const [category, setCategory] = useState("ceo"); const [stage, setStage] = useState("idle"); const [jobId, setJobId] = useState(null); const [progress, setProgress] = useState({ current: 0, total: 0, phase: "" }); const [results, setResults] = useState([]); const [enrichStage, setEnrichStage] = useState("idle"); const [enrichProgress, setEnrichProgress] = useState({ current: 0, total: 0 }); const { addJob, updateJob, removeJob } = useAppStore(); // Build queries: every keyword × every region const queries = keywords.length === 0 ? [] : regions.length > 0 ? keywords.flatMap(k => regions.map(r => `${k} ${r}`)) : keywords; const addKeyword = (k: string) => { const trimmed = k.trim(); if (trimmed && !keywords.includes(trimmed)) setKeywords(prev => [...prev, trimmed]); setKeywordInput(""); }; const removeKeyword = (k: string) => setKeywords(prev => prev.filter(x => x !== k)); const addRegion = (r: string) => { const trimmed = r.trim(); if (trimmed && !regions.includes(trimmed)) { setRegions(prev => [...prev, trimmed]); } setRegionInput(""); }; const removeRegion = (r: string) => setRegions(prev => prev.filter(x => x !== r)); const startJob = async () => { if (!keywords.length) return toast.error("Mindestens einen Suchbegriff eingeben"); setStage("running"); setResults([]); setProgress({ current: 0, total: queries.length, phase: "Searching Google Maps..." }); try { const res = await fetch("/api/jobs/maps-enrich", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ queries, maxResultsPerQuery: Number(maxResults), languageCode: "de", categories: [category], enrichEmails, }), }); const data = await res.json() as { jobId?: string; error?: string }; if (!res.ok || !data.jobId) throw new Error(data.error || "Failed"); setJobId(data.jobId); addJob({ id: data.jobId, type: "maps", status: "running", progress: 0, total: queries.length * Number(maxResults) }); pollJob(data.jobId); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to start"); setStage("failed"); } }; const pollJob = (id: string) => { let phase = "Searching Google Maps..."; 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; results: ResultRow[]; error?: string; }; if (data.totalLeads > 0 && data.emailsFound === 0 && enrichEmails) { phase = "Anreicherung mit Anymailfinder..."; } if (data.emailsFound > 0) { phase = `${data.emailsFound} E-Mails gefunden...`; } setProgress({ current: enrichEmails ? data.emailsFound : data.totalLeads, total: enrichEmails ? data.totalLeads : queries.length * Number(maxResults), phase, }); if (data.results?.length) setResults(data.results); updateJob(id, { status: data.status, progress: data.emailsFound, total: data.totalLeads }); if (data.status === "complete" || data.status === "failed") { clearInterval(interval); removeJob(id); setResults(data.results || []); if (data.status === "complete") { setStage("done"); const msg = enrichEmails ? `Fertig! ${data.totalLeads} Unternehmen gefunden, ${data.emailsFound} E-Mails angereichert` : `Fertig! ${data.totalLeads} Unternehmen gefunden`; toast.success(msg); } else { setStage("failed"); const errMsg = data.error || "Unbekannter Fehler"; toast.error(`Job fehlgeschlagen: ${errMsg}`); } } } catch { clearInterval(interval); setStage("failed"); } }, 2000); }; const startEnrichment = async () => { const companies = results .filter(r => r.domain) .map(r => ({ name: r.companyName || "", domain: r.domain || "" })); if (!companies.length) return toast.error("Keine Domains in den Ergebnissen"); setEnrichStage("running"); setEnrichProgress({ current: 0, total: companies.length }); try { const res = await fetch("/api/jobs/airscale-enrich", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ companies, categories: [category] }), }); const data = await res.json() as { jobId?: string; error?: string }; if (!res.ok || !data.jobId) throw new Error(data.error || "Failed"); addJob({ id: data.jobId, type: "airscale", status: "running", progress: 0, total: companies.length }); pollEnrichJob(data.jobId); } catch (err) { toast.error(err instanceof Error ? err.message : "Anreicherung konnte nicht gestartet werden"); setEnrichStage("failed"); } }; const pollEnrichJob = (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; results: ResultRow[]; error?: string; }; setEnrichProgress({ current: data.emailsFound, total: data.totalLeads }); updateJob(id, { status: data.status, progress: data.emailsFound, total: data.totalLeads }); if (data.status === "complete" || data.status === "failed") { clearInterval(interval); removeJob(id); setEnrichStage(data.status === "complete" ? "done" : "failed"); if (data.status === "complete") { const emailMap = new Map( (data.results || []).filter(r => r.email).map(r => [r.domain, r]) ); setResults(prev => prev.map(r => { const enriched = r.domain ? emailMap.get(r.domain) : undefined; return enriched ? { ...r, email: enriched.email, contactName: enriched.contactName, contactTitle: enriched.contactTitle } : r; })); toast.success(`${data.emailsFound} Entscheider-Emails gefunden`); } else { toast.error(`Anreicherung fehlgeschlagen: ${data.error || "Unbekannter Fehler"}`); } } } catch { clearInterval(interval); setEnrichStage("failed"); } }, 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, })); const emailsFound = results.filter(r => r.email).length; const totalExpected = queries.length * Number(maxResults); return (
{/* Header */}
Tab 4 Google Maps

Google Maps → Email

Finde lokale Unternehmen über Google Maps und bereichere sie mit Entscheider-Emails.

{/* Info banner */}

Nutzt die Google Maps Places API (New). Max. 60 Ergebnisse pro Suchanfrage. Füge mehrere Regionen hinzu um mehr Ergebnisse zu erhalten. $200 Free Credit/Monat ≈ ~6.000 kostenlose Searches.

{/* Step 1: Search config */}

1 Suchanfrage konfigurieren

{/* Keywords */}
{keywords.map(k => ( {k} ))} setKeywordInput(e.target.value)} onKeyDown={e => { if (e.key === "Enter" || e.key === ",") { e.preventDefault(); addKeyword(keywordInput); } }} onBlur={() => keywordInput && addKeyword(keywordInput)} />
{/* Keyword presets */}
{KEYWORD_PRESETS.filter(k => !keywords.includes(k)).map(k => ( ))}
{/* Regions */}
{/* Region chips */}
{regions.map(r => ( {r} ))} setRegionInput(e.target.value)} onKeyDown={e => { if (e.key === "Enter" || e.key === ",") { e.preventDefault(); addRegion(regionInput); } }} onBlur={() => regionInput && addRegion(regionInput)} />
{/* Region presets */}
{GERMAN_REGIONS.filter(r => !regions.includes(r)).map(r => ( ))}
{/* Max results + language */}

= bis zu{" "} {totalExpected.toLocaleString()}{" "} Ergebnisse total ({queries.length} {queries.length === 1 ? "Query" : "Queries"})

{/* Preview queries */} {queries.length > 0 && (

Suchanfragen die ausgeführt werden:

{queries.slice(0, 5).map((q, i) => (
{i+1} "{q}"
))} {queries.length > 5 && (

+{queries.length - 5} weitere...

)}
)}
{/* Step 2: Email enrichment */}

2 Email Enrichment

setEnrichEmails(!!v)} className="border-[#2e2e3e] mt-0.5" />

Entscheider-Emails für alle gefundenen Domains suchen (2 Credits/gültige Email)

{enrichEmails && (
{CATEGORY_OPTIONS.map(opt => ( ))}
)}
{/* Step 3: Run */}

3 Starten

{stage === "idle" || stage === "failed" ? ( ) : stage === "running" ? ( ) : ( )}
{/* Results */} {results.length > 0 && (

Ergebnisse ({results.length} Unternehmen{emailsFound > 0 ? `, ${emailsFound} Emails` : ""})

0 ? `${emailsFound} E-Mails gefunden` : `${results.length} Unternehmen`} />
{/* Post-enrichment CTA */} {stage === "done" && !enrichEmails && enrichStage === "idle" && (

{results.length} Unternehmen gefunden — jetzt Entscheider-Emails suchen?

Über Anymailfinder · 2 Credits pro gefundener Email

{CATEGORY_OPTIONS.map(opt => ( ))}
)} {enrichStage === "running" && ( )} {enrichStage === "done" && ( )}
)} {stage === "idle" && ( )}
); }