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>
This commit is contained in:
TimoUttenweiler
2026-04-01 10:25:43 +02:00
parent c232f0cb79
commit aa6707b8bc
7 changed files with 531 additions and 145 deletions

View File

@@ -35,33 +35,25 @@ export async function sinkLeadToVault(
const email = lead.email || null;
if (domain) {
// Check for exact duplicate (same domain + same email)
if (email) {
const exact = await prisma.lead.findFirst({
where: { domain, email },
});
if (exact) return exact.id; // already exists, skip
}
// Strict domain dedup: one lead per domain
const existing = await prisma.lead.findFirst({ where: { domain } });
// Check if same domain exists without email and we now have one
const existing = await prisma.lead.findFirst({
where: { domain, email: null },
});
if (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;
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
}
}
@@ -107,25 +99,27 @@ export async function sinkLeadsToVault(
const email = lead.email || null;
if (domain) {
if (email) {
const exact = await prisma.lead.findFirst({ where: { domain, email } });
if (exact) { skipped++; continue; }
}
// Strict domain dedup: one lead per domain
const existing = await prisma.lead.findFirst({ where: { domain } });
const existing = await prisma.lead.findFirst({ where: { domain, email: null } });
if (existing && email) {
await prisma.lead.update({
where: { id: existing.id },
data: {
email,
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++;
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;
}
}
@@ -140,7 +134,7 @@ export async function sinkLeadsToVault(
linkedinUrl: lead.linkedinUrl || null,
phone: lead.phone || null,
address: lead.address || null,
serpTitle: lead.serpTitle || null,
serpTitle: lead.serpTitle || null,
serpSnippet: lead.serpSnippet || null,
serpRank: lead.serpRank ?? null,
serpUrl: lead.serpUrl || null,