- 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>
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getApiKey } from "@/lib/utils/apiKey";
|
|
import { runGoogleSerpScraper, pollRunStatus, fetchDatasetItems } from "@/lib/services/apify";
|
|
import { isSocialOrDirectory } from "@/lib/utils/domains";
|
|
import { sinkLeadsToVault } from "@/lib/services/leadVault";
|
|
|
|
function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)); }
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const body = await req.json() as {
|
|
query: string;
|
|
count: number;
|
|
country: string;
|
|
language: string;
|
|
filterSocial: boolean;
|
|
};
|
|
|
|
const { query, count, country, language, filterSocial } = body;
|
|
if (!query?.trim()) return NextResponse.json({ error: "Kein Suchbegriff" }, { status: 400 });
|
|
|
|
const apifyToken = await getApiKey("apify");
|
|
if (!apifyToken) return NextResponse.json({ error: "Apify API-Key fehlt" }, { status: 400 });
|
|
|
|
const maxPages = Math.ceil(count / 10);
|
|
const runId = await runGoogleSerpScraper(query, maxPages, country, language, apifyToken);
|
|
|
|
let runStatus = "";
|
|
let datasetId = "";
|
|
while (runStatus !== "SUCCEEDED" && runStatus !== "FAILED" && runStatus !== "ABORTED") {
|
|
await sleep(3000);
|
|
const result = await pollRunStatus(runId, apifyToken);
|
|
runStatus = result.status;
|
|
datasetId = result.defaultDatasetId;
|
|
}
|
|
|
|
if (runStatus !== "SUCCEEDED") throw new Error(`Apify run ${runStatus}`);
|
|
|
|
let serpResults = await fetchDatasetItems(datasetId, apifyToken);
|
|
|
|
if (filterSocial) {
|
|
serpResults = serpResults.filter(r => !isSocialOrDirectory(r.domain));
|
|
}
|
|
|
|
// Deduplicate by domain
|
|
const seen = new Set<string>();
|
|
const unique = serpResults.filter(r => {
|
|
if (!r.domain || seen.has(r.domain)) return false;
|
|
seen.add(r.domain);
|
|
return true;
|
|
}).slice(0, count);
|
|
|
|
const stats = await sinkLeadsToVault(
|
|
unique.map((r, i) => ({
|
|
domain: r.domain,
|
|
companyName: r.title || null,
|
|
serpTitle: r.title || null,
|
|
serpSnippet: r.description || null,
|
|
serpRank: r.position ?? i + 1,
|
|
serpUrl: r.url || null,
|
|
})),
|
|
"quick-serp",
|
|
query,
|
|
);
|
|
|
|
return NextResponse.json(stats);
|
|
} catch (err) {
|
|
console.error("POST /api/leads/quick-serp error:", err);
|
|
return NextResponse.json({ error: err instanceof Error ? err.message : "Fehler" }, { status: 500 });
|
|
}
|
|
}
|