mein-solar: full feature set

- Schema: companyType, topics, salesScore, salesReason, offerPackage, approved, approvedAt, SearchHistory table
- /api/search-history: GET (by mode) + POST (save query)
- /api/ai-search: stadtwerke/industrie/custom prompts with history dedup
- /api/enrich-leads: website scraping + GPT-4o-mini enrichment (fire-and-forget after each job)
- /api/generate-email: personalized outreach via GPT-4o
- Suche page: 3 mode tabs (Stadtwerke/Industrie/Freie Suche), Alle-Bundesländer queue button, AiSearchModal gets searchMode + history
- Leadspeicher: Bewertung dots column, Paket badge column, Freigeben toggle button, email generator in SidePanel, approved-only export option
- Leads API: approvedOnly + companyType filters, new fields in PATCH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Timo Uttenweiler
2026-04-08 21:06:07 +02:00
parent e5172cbdc5
commit 54e0d22f9c
14 changed files with 866 additions and 47 deletions

View File

@@ -0,0 +1,108 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
const EMAIL_PROMPT_TEMPLATE = `Du schreibst eine professionelle Erstansprache-E-Mail für mein-solar.com an einen potenziellen Kunden.
mein-solar.com ist ein Full-Service EPC-Anbieter für Photovoltaik (5 kW bis 10+ MW), Batteriespeicher und Ladeinfrastruktur.
Firmendaten des Empfängers:
- Unternehmen: [companyName]
- Ansprechpartner: [contactName], [contactTitle]
- Unternehmenstyp: [companyType]
- Themen des Unternehmens: [topics]
- Empfohlenes Paket: [offerPackage]
Schreibe eine E-Mail auf Deutsch:
- Betreff: prägnant, bezieht sich auf konkretes Thema des Unternehmens
- Anrede: personalisiert wenn Name bekannt
- 2-3 kurze Absätze: Bezug auf das Unternehmen → Mehrwert von mein-solar → konkreter nächster Schritt (Erstgespräch)
- Ton: professionell, direkt, kein Marketing-Sprech
- Keine Buzzwords wie "innovativ" oder "nachhaltig"
- Signatur: [Vorname] von mein-solar.com
Antworte mit JSON: { "subject": "...", "body": "..." }`;
export async function POST(req: NextRequest) {
try {
const { leadId } = await req.json() as { leadId: string };
if (!leadId) {
return NextResponse.json({ error: "leadId fehlt" }, { status: 400 });
}
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
return NextResponse.json({ error: "OpenRouter API Key nicht konfiguriert" }, { status: 500 });
}
const lead = await prisma.lead.findUnique({ where: { id: leadId } });
if (!lead) {
return NextResponse.json({ error: "Lead nicht gefunden" }, { status: 404 });
}
if (!lead.email) {
return NextResponse.json({ error: "Lead hat keine E-Mail-Adresse" }, { status: 400 });
}
let topicsText = "";
if (lead.topics) {
try {
const topicsArr = JSON.parse(lead.topics) as string[];
topicsText = topicsArr.join(", ");
} catch {
topicsText = lead.topics;
}
}
const prompt = EMAIL_PROMPT_TEMPLATE
.replace("[companyName]", lead.companyName || "Unbekannt")
.replace("[contactName]", lead.contactName || "")
.replace("[contactTitle]", lead.contactTitle || "")
.replace("[companyType]", lead.companyType || "")
.replace("[topics]", topicsText || "")
.replace("[offerPackage]", lead.offerPackage || "");
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://mein-solar.com",
"X-Title": "MeinSolar Leads",
},
body: JSON.stringify({
model: "openai/gpt-4o",
temperature: 0.5,
max_tokens: 600,
messages: [
{ role: "user", content: prompt },
],
}),
});
if (!res.ok) {
const err = await res.text();
console.error("[generate-email] OpenRouter error:", err);
return NextResponse.json({ error: "KI-Anfrage fehlgeschlagen" }, { status: 500 });
}
const data = await res.json() as { choices: Array<{ message: { content: string } }> };
const raw = data.choices[0]?.message?.content?.trim() ?? "";
let email: { subject: string; body: string };
try {
email = JSON.parse(raw) as { subject: string; body: string };
} catch {
const match = raw.match(/\{[\s\S]*\}/);
if (!match) {
return NextResponse.json({ error: "KI-Antwort konnte nicht geparst werden" }, { status: 500 });
}
email = JSON.parse(match[0]) as { subject: string; body: string };
}
return NextResponse.json({ subject: email.subject, body: email.body });
} catch (err) {
console.error("POST /api/generate-email error:", err);
return NextResponse.json({ error: "E-Mail-Generierung fehlgeschlagen" }, { status: 500 });
}
}