Files
lead-scraper/lib/services/leadVault.ts
TimoUttenweiler aa6707b8bc feat: Kundensuche – Progressbar, SERP-Supplement, Dedup, Löschen, Neu-Filter
- Progressbar geht nie mehr rückwärts (Math.min-Cap entfernt)
- E-Mails-suchen-Phase wird immer kurz angezeigt bevor Fertig
- SERP-Supplement startet automatisch wenn Maps < Zielanzahl liefert
- Suchergebnisse bleiben nach Abschluss sichtbar (kein Redirect)
- Dedup in leadVault strikt nach Domain (verhindert Duplikate)
- isNew-Flag pro Result (Batch-Query gegen bestehende Vault-Domains)
- Nur-neue-Filter + vorhanden-Badge in Suchergebnissen
- Einzeln und Bulk löschen aus Suchergebnissen + Leadspeicher
- Fehlermeldung zeigt echten API-Fehler (z.B. 402 Anymailfinder)
- SERP-Supplement aus /api/search entfernt (LoadingCard übernimmt)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 10:25:43 +02:00

157 lines
4.6 KiB
TypeScript

import { prisma } from "@/lib/db";
export interface VaultLead {
domain?: string | null;
companyName?: string | null;
contactName?: string | null;
contactTitle?: string | null;
email?: string | null;
linkedinUrl?: string | null;
phone?: string | null;
address?: string | null;
serpTitle?: string | null;
serpSnippet?: string | null;
serpRank?: number | null;
serpUrl?: string | null;
country?: string | null;
headcount?: string | null;
industry?: string | null;
description?: string | null;
}
/**
* Deduplication logic:
* - Same domain + same email → skip (already exists)
* - Same domain + no email yet + new version has email → update
* - Otherwise → create new lead
*/
export async function sinkLeadToVault(
lead: VaultLead,
sourceTab: string,
sourceTerm?: string,
sourceJobId?: string,
): Promise<string> {
const domain = lead.domain || null;
const email = lead.email || null;
if (domain) {
// Strict domain dedup: one lead per domain
const existing = await prisma.lead.findFirst({ where: { domain } });
if (existing) {
if (email && !existing.email) {
// Upgrade: fill in email + other missing fields
await prisma.lead.update({
where: { id: existing.id },
data: {
email,
contactName: lead.contactName || existing.contactName,
contactTitle: lead.contactTitle || existing.contactTitle,
linkedinUrl: lead.linkedinUrl || existing.linkedinUrl,
phone: lead.phone || existing.phone,
updatedAt: new Date(),
},
});
}
return existing.id; // domain already exists → skip or upgraded
}
}
const created = await prisma.lead.create({
data: {
domain,
companyName: lead.companyName || null,
contactName: lead.contactName || null,
contactTitle: lead.contactTitle || null,
email,
linkedinUrl: lead.linkedinUrl || null,
phone: lead.phone || null,
address: lead.address || null,
serpTitle: lead.serpTitle || null,
serpSnippet: lead.serpSnippet || null,
serpRank: lead.serpRank ?? null,
serpUrl: lead.serpUrl || null,
country: lead.country || null,
headcount: lead.headcount || null,
industry: lead.industry || null,
description: lead.description || null,
sourceTab,
sourceTerm: sourceTerm || null,
sourceJobId: sourceJobId || null,
status: "new",
priority: "normal",
},
});
return created.id;
}
export async function sinkLeadsToVault(
leads: VaultLead[],
sourceTab: string,
sourceTerm?: string,
sourceJobId?: string,
): Promise<{ added: number; updated: number; skipped: number }> {
let added = 0, updated = 0, skipped = 0;
for (const lead of leads) {
const domain = lead.domain || null;
const email = lead.email || null;
if (domain) {
// Strict domain dedup: one lead per domain
const existing = await prisma.lead.findFirst({ where: { domain } });
if (existing) {
if (email && !existing.email) {
// Upgrade: existing has no email, new one does → fill it in
await prisma.lead.update({
where: { id: existing.id },
data: {
email,
contactName: lead.contactName || existing.contactName,
contactTitle: lead.contactTitle || existing.contactTitle,
linkedinUrl: lead.linkedinUrl || existing.linkedinUrl,
phone: lead.phone || existing.phone,
updatedAt: new Date(),
},
});
updated++;
} else {
skipped++; // domain already exists → skip
}
continue;
}
}
await prisma.lead.create({
data: {
domain,
companyName: lead.companyName || null,
contactName: lead.contactName || null,
contactTitle: lead.contactTitle || null,
email,
linkedinUrl: lead.linkedinUrl || null,
phone: lead.phone || null,
address: lead.address || null,
serpTitle: lead.serpTitle || null,
serpSnippet: lead.serpSnippet || null,
serpRank: lead.serpRank ?? null,
serpUrl: lead.serpUrl || null,
country: lead.country || null,
headcount: lead.headcount || null,
industry: lead.industry || null,
description: lead.description || null,
sourceTab,
sourceTerm: sourceTerm || null,
sourceJobId: sourceJobId || null,
status: "new",
priority: "normal",
},
});
added++;
}
return { added, updated, skipped };
}