feat(crm): implement bulk mail action for contacts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🏗️ Build (push) Failing after 6m25s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🏗️ Build (push) Failing after 6m25s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
- Add BulkMailButton component for Payload list view - Add bulkMailEndpoint handler for processing prompts - Integrate bulk mail action into CrmContacts collection - Update dev:clean script to skip interactive prompts
This commit is contained in:
191
apps/web/src/payload/endpoints/bulkMailEndpoint.ts
Normal file
191
apps/web/src/payload/endpoints/bulkMailEndpoint.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { PayloadRequest } from "payload";
|
||||
|
||||
export const bulkMailEndpointHandler = async (req: PayloadRequest) => {
|
||||
try {
|
||||
let body: any = {};
|
||||
if (req.body) {
|
||||
try {
|
||||
body = (await req.json?.()) || {};
|
||||
} catch (_e) {
|
||||
body = req.body;
|
||||
}
|
||||
}
|
||||
|
||||
const { contactIds, instructions, isTest } = body;
|
||||
|
||||
if (!req.user) {
|
||||
return Response.json(
|
||||
{ success: false, error: "Unauthorized" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(contactIds) || contactIds.length === 0) {
|
||||
return Response.json(
|
||||
{ success: false, error: "No contacts selected" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
if (!OPENROUTER_KEY) {
|
||||
return Response.json(
|
||||
{ success: false, error: "Missing OPENROUTER_API_KEY" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const sentEmails = [];
|
||||
const interactions = [];
|
||||
|
||||
for (const contactId of contactIds) {
|
||||
// Fetch contact with account populated
|
||||
const contact = await req.payload.findByID({
|
||||
collection: "crm-contacts",
|
||||
id: contactId,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
if (!contact || !contact.email) continue;
|
||||
|
||||
const account = contact.account as any;
|
||||
const accountInfo = account
|
||||
? `
|
||||
Company Name: ${account.name || "Unknown"}
|
||||
Website: ${account.website || "Unknown"}
|
||||
Industry: ${account.industry || "Unknown"}
|
||||
Website Status: ${account.websiteStatus || "Unknown"}
|
||||
Internal Notes: ${account.notes || "None"}
|
||||
`
|
||||
: "No company information available.";
|
||||
|
||||
const prompt = `You are an expert sales/business development AI assistant. Write a professional, personalized German B2B outreach email ("Anschreiben").
|
||||
|
||||
CONTEXT ABOUT THE CONTACT:
|
||||
Name: ${contact.fullName || contact.firstName || "Unknown"}
|
||||
Role: ${contact.role || "Unknown"}
|
||||
|
||||
CONTEXT ABOUT THEIR COMPANY:
|
||||
${accountInfo}
|
||||
|
||||
USER INSTRUCTIONS / GOAL OF THE EMAIL:
|
||||
${instructions}
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
1. Write the email subject on the FIRST line as: "SUBJECT: <Your Subject>"
|
||||
2. The rest of the message should be the email body.
|
||||
3. Keep it professional and natural in German (Sie-form unless you think Du is appropriate based on the industry, but prefer Sie).
|
||||
4. Output only the email text, no markdown code blocks around it. No extra chit-chat.
|
||||
`;
|
||||
|
||||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${OPENROUTER_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "google/gemini-3-flash-preview",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
const generatedText = data.choices?.[0]?.message?.content?.trim() || "";
|
||||
|
||||
if (!generatedText) {
|
||||
console.error("AI Generation failed for contact", contactId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse subject and body
|
||||
const lines = generatedText.split("\n");
|
||||
let subject = "Information von Mintel";
|
||||
let bodyText = generatedText;
|
||||
|
||||
if (lines[0].startsWith("SUBJECT:")) {
|
||||
subject = lines[0].replace("SUBJECT:", "").trim();
|
||||
bodyText = lines.slice(1).join("\n").trim();
|
||||
}
|
||||
|
||||
// Use a simple HTML wrapper to preserve line breaks
|
||||
const htmlBody = `<p>${bodyText.replace(/\n/g, "<br/>")}</p>`;
|
||||
|
||||
const targetEmail = isTest ? req.user.email : contact.email;
|
||||
|
||||
// Send the email
|
||||
await req.payload.sendEmail({
|
||||
to: targetEmail,
|
||||
subject: (isTest ? "[TEST] " : "") + subject,
|
||||
html: htmlBody,
|
||||
});
|
||||
|
||||
sentEmails.push({ contactId, email: targetEmail });
|
||||
|
||||
if (!isTest) {
|
||||
// Log interaction
|
||||
const interaction = await req.payload.create({
|
||||
collection: "crm-interactions",
|
||||
data: {
|
||||
type: "email",
|
||||
subject: `Outbound AI Mail: ${subject}`,
|
||||
date: new Date().toISOString(),
|
||||
contact: contact.id,
|
||||
account: account?.id,
|
||||
content: {
|
||||
root: {
|
||||
type: "root",
|
||||
format: "",
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: "ltr",
|
||||
children: [
|
||||
{
|
||||
type: "paragraph",
|
||||
format: "",
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: "ltr",
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
mode: "normal",
|
||||
style: "",
|
||||
detail: 0,
|
||||
format: 0,
|
||||
text: bodyText,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
interactions.push(interaction.id);
|
||||
|
||||
// Update lead temperature to "warm" if it's currently cold and the status is lead
|
||||
if (
|
||||
account &&
|
||||
account.status === "lead" &&
|
||||
account.leadTemperature === "cold"
|
||||
) {
|
||||
await req.payload.update({
|
||||
collection: "crm-accounts",
|
||||
id: account.id,
|
||||
data: {
|
||||
leadTemperature: "warm",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ success: true, sentEmails, testMode: isTest });
|
||||
} catch (e: any) {
|
||||
console.error("Bulk Mail error", e);
|
||||
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user