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:
@@ -5,6 +5,7 @@
|
||||
// Bulk API processes ~1,000 rows per 5 minutes asynchronously.
|
||||
|
||||
import axios from "axios";
|
||||
import { extractDomainFromUrl } from "@/lib/utils/domains";
|
||||
|
||||
const BASE_URL = "https://api.anymailfinder.com/v5.1";
|
||||
|
||||
@@ -141,6 +142,7 @@ export async function getBulkSearchStatus(
|
||||
/**
|
||||
* Download bulk search results as JSON array.
|
||||
* IMPORTANT: Credits are charged on first download.
|
||||
* Handles both array-of-objects and array-of-arrays (with header row) formats.
|
||||
*/
|
||||
export async function downloadBulkResults(
|
||||
searchId: string,
|
||||
@@ -151,7 +153,21 @@ export async function downloadBulkResults(
|
||||
headers: { Authorization: apiKey },
|
||||
timeout: 60000,
|
||||
});
|
||||
return response.data as Array<Record<string, string>>;
|
||||
|
||||
const raw = response.data;
|
||||
|
||||
// Handle array-of-arrays format (first row = headers)
|
||||
if (Array.isArray(raw) && Array.isArray(raw[0])) {
|
||||
const headers = (raw[0] as string[]).map(h => h.toLowerCase().trim());
|
||||
return (raw as string[][]).slice(1).map(row => {
|
||||
const obj: Record<string, string> = {};
|
||||
headers.forEach((h, i) => { obj[h] = row[i] ?? ""; });
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
// Standard array-of-objects format
|
||||
return raw as Array<Record<string, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,20 +212,35 @@ export async function bulkSearchDomains(
|
||||
const rows = await downloadBulkResults(searchId, apiKey);
|
||||
|
||||
// 4. Normalize to DecisionMakerResult[]
|
||||
// Log first row to help debug field names
|
||||
if (rows.length > 0) {
|
||||
console.log("[Anymailfinder] Raw response sample row keys:", Object.keys(rows[0]));
|
||||
console.log("[Anymailfinder] Sample row:", JSON.stringify(rows[0]));
|
||||
}
|
||||
|
||||
return rows.map(row => {
|
||||
const rawDomain = row["domain"] || row["Domain"] || "";
|
||||
const domain = rawDomain ? extractDomainFromUrl(rawDomain.includes(".") ? rawDomain : `https://${rawDomain}`) : "";
|
||||
|
||||
// Anymailfinder bulk JSON uses different field names than the single-search API:
|
||||
// amf_status (not email_status), valid_email_only (not valid_email),
|
||||
// person_name (not person_full_name), result_title (not person_job_title),
|
||||
// linkedin_url (not person_linkedin_url)
|
||||
const email = row["email"] || row["Email"] || null;
|
||||
const emailStatus = (row["email_status"] || row["Email Status"] || "not_found").toLowerCase();
|
||||
const validEmail = emailStatus === "valid" ? email : null;
|
||||
const rawStatus = row["amf_status"] || row["email_status"] || row["Email Status"] || "not_found";
|
||||
// amf_status uses "not found" (space) — normalize to underscore variant
|
||||
const emailStatus = rawStatus.toLowerCase().replace(/\s+/g, "_") as DecisionMakerResult["email_status"];
|
||||
const validEmail = row["valid_email_only"] || row["valid_email"] || (emailStatus === "valid" ? email : null) || null;
|
||||
|
||||
return {
|
||||
domain: row["domain"] || row["Domain"] || "",
|
||||
domain,
|
||||
decision_maker_category: primaryCategory,
|
||||
email,
|
||||
email_status: emailStatus as DecisionMakerResult["email_status"],
|
||||
email: email || validEmail,
|
||||
email_status: emailStatus,
|
||||
valid_email: validEmail,
|
||||
person_full_name: row["person_full_name"] || row["Full Name"] || null,
|
||||
person_job_title: row["person_job_title"] || row["Job Title"] || null,
|
||||
person_linkedin_url: row["person_linkedin_url"] || row["LinkedIn URL"] || null,
|
||||
person_full_name: row["person_name"] || row["person_full_name"] || row["Full Name"] || null,
|
||||
person_job_title: row["result_title"] || row["person_job_title"] || row["Job Title"] || null,
|
||||
person_linkedin_url: row["linkedin_url"] || row["person_linkedin_url"] || row["LinkedIn URL"] || null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user