feat: LeadVault - zentrale Lead-Datenbank mit CRM-Funktionen
- Prisma-Schema: Lead + LeadEvent Modelle (Migration 20260320) - lib/services/leadVault.ts: sinkLeadsToVault mit Deduplizierung - Auto-Sync: alle 4 Pipelines schreiben Leads in LeadVault - GET /api/leads: Filter, Sortierung, Pagination (Server-side) - PATCH/DELETE /api/leads/[id]: Status, Priorität, Tags, Notizen - POST /api/leads/bulk: Bulk-Aktionen für mehrere Leads - GET /api/leads/stats: Statistiken + 7-Tage-Sparkline - POST /api/leads/quick-serp: SERP-Capture ohne Enrichment - GET /api/leads/export: CSV-Export mit allen Feldern - app/leadvault/page.tsx: vollständige UI mit Stats, Quick SERP, Filter-Leiste, sortierbare Tabelle, Bulk-Aktionen, Side Panel - Sidebar: LeadVault-Eintrag mit Live-Badge (neue Leads) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { prisma } from "@/lib/db";
|
||||
import { getApiKey } from "@/lib/utils/apiKey";
|
||||
import { cleanDomain } from "@/lib/utils/domains";
|
||||
import { bulkSearchDomains, type DecisionMakerCategory } from "@/lib/services/anymailfinder";
|
||||
import { sinkLeadsToVault } from "@/lib/services/leadVault";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -93,6 +94,22 @@ async function runEnrichment(
|
||||
where: { id: jobId },
|
||||
data: { status: "complete", emailsFound, totalLeads: results.length },
|
||||
});
|
||||
|
||||
// Sync to LeadVault
|
||||
await sinkLeadsToVault(
|
||||
results.map(r => ({
|
||||
domain: r.domain || null,
|
||||
companyName: domainMap.get(r.domain || "") || null,
|
||||
contactName: r.person_full_name || null,
|
||||
contactTitle: r.person_job_title || null,
|
||||
email: r.email || null,
|
||||
linkedinUrl: r.person_linkedin_url || null,
|
||||
emailConfidence: r.valid_email ? 1.0 : r.email_status === "risky" ? 0.5 : 0,
|
||||
})),
|
||||
"airscale",
|
||||
undefined,
|
||||
jobId,
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await prisma.job.update({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getApiKey } from "@/lib/utils/apiKey";
|
||||
import { sinkLeadsToVault } from "@/lib/services/leadVault";
|
||||
import {
|
||||
submitBulkPersonSearch,
|
||||
getBulkSearchStatus,
|
||||
@@ -150,6 +151,23 @@ async function runLinkedInEnrich(
|
||||
where: { id: enrichJobId },
|
||||
data: { status: "complete", emailsFound },
|
||||
});
|
||||
|
||||
// Sync final state to LeadVault
|
||||
const finalResults = await prisma.leadResult.findMany({ where: { jobId: enrichJobId } });
|
||||
await sinkLeadsToVault(
|
||||
finalResults.map(r => ({
|
||||
domain: r.domain,
|
||||
companyName: r.companyName,
|
||||
contactName: r.contactName,
|
||||
contactTitle: r.contactTitle,
|
||||
email: r.email,
|
||||
linkedinUrl: r.linkedinUrl,
|
||||
emailConfidence: r.confidence,
|
||||
})),
|
||||
"linkedin",
|
||||
undefined,
|
||||
enrichJobId,
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await prisma.job.update({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { prisma } from "@/lib/db";
|
||||
import { getApiKey } from "@/lib/utils/apiKey";
|
||||
import { searchPlacesMultiQuery } from "@/lib/services/googlemaps";
|
||||
import { bulkSearchDomains, type DecisionMakerCategory } from "@/lib/services/anymailfinder";
|
||||
import { sinkLeadsToVault } from "@/lib/services/leadVault";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -147,6 +148,24 @@ async function runMapsEnrich(
|
||||
data: { status: "complete", totalLeads: places.length },
|
||||
});
|
||||
}
|
||||
|
||||
// Sync to LeadVault
|
||||
const finalResults = await prisma.leadResult.findMany({ where: { jobId } });
|
||||
await sinkLeadsToVault(
|
||||
finalResults.map(r => ({
|
||||
domain: r.domain,
|
||||
companyName: r.companyName,
|
||||
contactName: r.contactName,
|
||||
contactTitle: r.contactTitle,
|
||||
email: r.email,
|
||||
linkedinUrl: r.linkedinUrl,
|
||||
emailConfidence: r.confidence,
|
||||
phone: (() => { try { return JSON.parse(r.source || "{}").phone ?? null; } catch { return null; } })(),
|
||||
})),
|
||||
"maps",
|
||||
params.queries.join(", "),
|
||||
jobId,
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await prisma.job.update({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getApiKey } from "@/lib/utils/apiKey";
|
||||
import { isSocialOrDirectory } from "@/lib/utils/domains";
|
||||
import { runGoogleSerpScraper, pollRunStatus, fetchDatasetItems } from "@/lib/services/apify";
|
||||
import { bulkSearchDomains, type DecisionMakerCategory } from "@/lib/services/anymailfinder";
|
||||
import { sinkLeadsToVault } from "@/lib/services/leadVault";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -137,6 +138,29 @@ async function runSerpEnrich(
|
||||
where: { id: jobId },
|
||||
data: { status: "complete", emailsFound, totalLeads: enrichResults.length },
|
||||
});
|
||||
|
||||
// Sync to LeadVault
|
||||
await sinkLeadsToVault(
|
||||
enrichResults.map(r => {
|
||||
const serpData = serpMap.get(r.domain || "");
|
||||
return {
|
||||
domain: r.domain || null,
|
||||
companyName: serpData?.title || null,
|
||||
contactName: r.person_full_name || null,
|
||||
contactTitle: r.person_job_title || null,
|
||||
email: r.email || null,
|
||||
linkedinUrl: r.person_linkedin_url || null,
|
||||
emailConfidence: r.valid_email ? 1.0 : r.email_status === "risky" ? 0.5 : 0,
|
||||
serpTitle: serpData?.title || null,
|
||||
serpSnippet: serpData?.description || null,
|
||||
serpRank: serpData?.position ?? null,
|
||||
serpUrl: serpData?.url || null,
|
||||
};
|
||||
}),
|
||||
"serp",
|
||||
params.query,
|
||||
jobId,
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await prisma.job.update({
|
||||
|
||||
Reference in New Issue
Block a user