UI improvements: Leadspeicher, Maps enrichment, exports
- Rename LeadVault → Leadspeicher throughout (sidebar, topbar, page) - SidePanel: full lead detail view with contact, source, tags (read-only), Google Maps link for address - Tags: kontaktiert stored as tag (toggleable), favorit tag toggle - Remove Status column, StatusBadge dropdown, Priority feature - Remove Aktualisieren button from Leadspeicher - Bulk actions: remove status dropdown - Export: LeadVault Excel-only, clean columns, freeze row + autofilter - Export dropdown: click-based (fix overflow-hidden clipping) - ExportButtons: remove CSV, Excel only everywhere - Maps page: post-search Anymailfinder enrichment button - ProgressCard: "Suche läuft..." instead of "Warte auf Anymailfinder-Server..." - Quick SERP renamed to "Schnell neue Suche" - Results page: Excel export, always-enabled download button - Anymailfinder: fix bulk field names, array-of-arrays format - Apify: fix countryCode lowercase - API: sourceTerm search, contacted/favorite tag filters Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,8 @@ export default function MapsPage() {
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [progress, setProgress] = useState({ current: 0, total: 0, phase: "" });
|
||||
const [results, setResults] = useState<ResultRow[]>([]);
|
||||
const [enrichStage, setEnrichStage] = useState<Stage>("idle");
|
||||
const [enrichProgress, setEnrichProgress] = useState({ current: 0, total: 0 });
|
||||
const { addJob, updateJob, removeJob } = useAppStore();
|
||||
|
||||
// Build queries: every keyword × every region
|
||||
@@ -162,20 +164,74 @@ export default function MapsPage() {
|
||||
}, 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,
|
||||
confidence_score: r.confidence !== undefined ? Math.round(r.confidence * 100) : undefined,
|
||||
source_tab: "maps",
|
||||
job_id: jobId || "",
|
||||
found_at: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
const emailsFound = results.filter(r => r.email).length;
|
||||
const hitRate = results.length > 0 ? Math.round((emailsFound / results.length) * 100) : 0;
|
||||
const totalExpected = queries.length * Number(maxResults);
|
||||
|
||||
return (
|
||||
@@ -420,7 +476,7 @@ export default function MapsPage() {
|
||||
title="Fertig"
|
||||
current={enrichEmails ? emailsFound : results.length}
|
||||
total={results.length}
|
||||
subtitle={enrichEmails ? `Hit Rate: ${hitRate}%` : `${results.length} Unternehmen gefunden`}
|
||||
subtitle={enrichEmails ? `${emailsFound} E-Mails gefunden` : `${results.length} Unternehmen gefunden`}
|
||||
status="complete"
|
||||
/>
|
||||
)}
|
||||
@@ -431,14 +487,69 @@ export default function MapsPage() {
|
||||
<Card className="bg-[#111118] border-[#1e1e2e] p-6 space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<h2 className="text-base font-semibold text-white">
|
||||
Ergebnisse ({results.length} Unternehmen{enrichEmails && emailsFound > 0 ? `, ${emailsFound} Emails` : ""})
|
||||
Ergebnisse ({results.length} Unternehmen{emailsFound > 0 ? `, ${emailsFound} Emails` : ""})
|
||||
</h2>
|
||||
<ExportButtons
|
||||
rows={exportRows}
|
||||
filename={`maps-leads-${jobId?.slice(0, 8) || "export"}`}
|
||||
summary={enrichEmails ? `${emailsFound} Emails · ${hitRate}% Hit Rate` : `${results.length} Unternehmen`}
|
||||
summary={emailsFound > 0 ? `${emailsFound} E-Mails gefunden` : `${results.length} Unternehmen`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Post-enrichment CTA */}
|
||||
{stage === "done" && !enrichEmails && enrichStage === "idle" && (
|
||||
<div className="flex items-center justify-between gap-4 bg-green-500/5 border border-green-500/20 rounded-xl px-4 py-3">
|
||||
<div>
|
||||
<p className="text-sm text-white font-medium">{results.length} Unternehmen gefunden — jetzt Entscheider-Emails suchen?</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Über Anymailfinder · 2 Credits pro gefundener Email</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{CATEGORY_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setCategory(opt.value)}
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium border transition-all ${
|
||||
category === opt.value
|
||||
? "bg-green-500/20 text-green-300 border-green-500/40"
|
||||
: "bg-[#0d0d18] text-gray-500 border-[#2e2e3e] hover:border-green-500/30"
|
||||
}`}
|
||||
>
|
||||
{opt.label.split(" / ")[0]}
|
||||
{opt.recommended && <span className="ml-1 text-[9px] text-green-400">★</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={startEnrichment}
|
||||
className="bg-gradient-to-r from-green-500 to-teal-600 hover:from-green-600 hover:to-teal-700 text-white font-medium px-5 whitespace-nowrap"
|
||||
>
|
||||
Entscheider-Emails finden
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{enrichStage === "running" && (
|
||||
<ProgressCard
|
||||
title="Entscheider-Emails werden gesucht..."
|
||||
current={enrichProgress.current}
|
||||
total={enrichProgress.total}
|
||||
subtitle="Anymailfinder Bulk Enrichment"
|
||||
status="running"
|
||||
/>
|
||||
)}
|
||||
|
||||
{enrichStage === "done" && (
|
||||
<ProgressCard
|
||||
title="Anreicherung abgeschlossen"
|
||||
current={emailsFound}
|
||||
total={results.length}
|
||||
subtitle={`${emailsFound} Entscheider-Emails gefunden`}
|
||||
status="complete"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ResultsTable
|
||||
rows={results}
|
||||
loading={stage === "running" && results.length === 0}
|
||||
|
||||
Reference in New Issue
Block a user