La till mejlkamera i outreach
X-Lovable-Edit-ID: edt-a7b925c7-a3c0-44b8-8f5b-477a625634da Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { Sparkles, RefreshCw, Building2, Users, MapPin, Mail, Search, Copy, Check } from "lucide-react";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Sparkles, RefreshCw, Building2, Users, MapPin, Mail, Search, Copy, Check, Send } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
@@ -34,8 +35,52 @@ export function LeadsGenerator() {
|
|||||||
const [researchingLead, setResearchingLead] = useState<string | null>(null);
|
const [researchingLead, setResearchingLead] = useState<string | null>(null);
|
||||||
const [emailDialog, setEmailDialog] = useState<ResearchResult | null>(null);
|
const [emailDialog, setEmailDialog] = useState<ResearchResult | null>(null);
|
||||||
const [copiedField, setCopiedField] = useState<string | null>(null);
|
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||||
|
const [recipient, setRecipient] = useState("");
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRecipient("");
|
||||||
|
}, [emailDialog?.company_name]);
|
||||||
|
|
||||||
|
const sendEmail = async () => {
|
||||||
|
if (!emailDialog || !recipient) return;
|
||||||
|
setSending(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/send-outreach-email`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
to_email: recipient,
|
||||||
|
to_name: emailDialog.company_name,
|
||||||
|
subject: emailDialog.email.subject,
|
||||||
|
body: emailDialog.email.body,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || "Kunde inte skicka mejl");
|
||||||
|
toast({
|
||||||
|
title: "Mejl skickat!",
|
||||||
|
description: `Skickat från konditorivaror@gmail.com till ${recipient}`,
|
||||||
|
});
|
||||||
|
setEmailDialog(null);
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: "Fel vid utskick",
|
||||||
|
description: err instanceof Error ? err.message : "Okänt fel",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const generateLeads = async () => {
|
const generateLeads = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -318,6 +363,32 @@ export function LeadsGenerator() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="pt-4 border-t space-y-2">
|
||||||
|
<label className="text-sm font-medium">Skicka till (mottagarens mejl):</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="kontakt@företag.se"
|
||||||
|
value={recipient}
|
||||||
|
onChange={(e) => setRecipient(e.target.value)}
|
||||||
|
disabled={sending}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={sendEmail}
|
||||||
|
disabled={sending || !recipient || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient)}
|
||||||
|
>
|
||||||
|
{sending ? (
|
||||||
|
<><RefreshCw className="h-4 w-4 mr-2 animate-spin" /> Skickar...</>
|
||||||
|
) : (
|
||||||
|
<><Send className="h-4 w-4 mr-2" /> Skicka mejl</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Skickas från <strong>konditorivaror@gmail.com</strong> som Tiffany.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { z } from "npm:zod@3.23.8";
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Headers":
|
||||||
|
"authorization, x-client-info, apikey, content-type",
|
||||||
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const GATEWAY_URL = "https://connector-gateway.lovable.dev/brevo";
|
||||||
|
|
||||||
|
const BodySchema = z.object({
|
||||||
|
to_email: z.string().email(),
|
||||||
|
to_name: z.string().min(1).max(200),
|
||||||
|
subject: z.string().min(1).max(300),
|
||||||
|
body: z.string().min(1).max(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.serve(async (req) => {
|
||||||
|
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY");
|
||||||
|
const BREVO_API_KEY = Deno.env.get("BREVO_API_KEY");
|
||||||
|
if (!LOVABLE_API_KEY || !BREVO_API_KEY) {
|
||||||
|
throw new Error("Email service not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = BodySchema.safeParse(await req.json());
|
||||||
|
if (!parsed.success) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "Ogiltiga fält", details: parsed.error.flatten().fieldErrors }),
|
||||||
|
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { to_email, to_name, subject, body } = parsed.data;
|
||||||
|
|
||||||
|
const senderEmail = Deno.env.get("BREVO_SENDER_EMAIL") ?? "konditorivaror@gmail.com";
|
||||||
|
const senderName = "Tiffany – Lennart Svenssons Konditorivaror";
|
||||||
|
|
||||||
|
// Konvertera radbrytningar till <br> för HTML
|
||||||
|
const htmlBody = `
|
||||||
|
<div style="font-family: Georgia, serif; font-size: 15px; color:#3d2817; line-height:1.6; max-width:600px;">
|
||||||
|
${body
|
||||||
|
.split(/\n{2,}/)
|
||||||
|
.map((p) => `<p>${p.replace(/\n/g, "<br/>")}</p>`)
|
||||||
|
.join("")}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const res = await fetch(`${GATEWAY_URL}/v3/smtp/email`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${LOVABLE_API_KEY}`,
|
||||||
|
"X-Connection-Api-Key": BREVO_API_KEY,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sender: { name: senderName, email: senderEmail },
|
||||||
|
to: [{ email: to_email, name: to_name }],
|
||||||
|
replyTo: { email: senderEmail, name: senderName },
|
||||||
|
subject,
|
||||||
|
htmlContent: htmlBody,
|
||||||
|
textContent: body,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errBody = await res.text();
|
||||||
|
console.error(`Brevo send failed [${res.status}]:`, errBody);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "Brevo avvisade mejlet", status: res.status, details: errBody }),
|
||||||
|
{ status: res.status, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return new Response(JSON.stringify({ success: true, messageId: data.messageId }), {
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("send-outreach-email error:", err);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: err?.message ?? "Kunde inte skicka mejl" }),
|
||||||
|
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user