import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { apiClient } from "../lib/apiClient"; import { useParams, Link } from "react-router-dom"; import { ArrowLeft, Megaphone, Layers, Mail, Users, BarChart2, Settings as SettingsIcon, Plus, Trash2, Play, Pause, Clock, CheckCircle2, Sparkles, Download, Send, Split, RefreshCw } from "lucide-react"; import { useState } from "react"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; import StatusBadge from "../components/StatusBadge"; import StatCard from "../components/StatCard"; import RichTextEditor from "../components/RichTextEditor"; import FormDrawer from "../components/FormDrawer"; import CsvExportButton from "../components/CsvExportButton"; import toast from "react-hot-toast"; export default function CampaignDetail() { const { id } = useParams<{ id: string }>(); const queryClient = useQueryClient(); const [activeTab, setActiveTab] = useState<"overview" | "sequences" | "accounts" | "leads" | "analytics" | "settings">("overview"); const [isAddLeadOpen, setIsAddLeadOpen] = useState(false); const [selectedVariantTab, setSelectedVariantTab] = useState>({}); const { data: campaign, isLoading, isError, refetch: refetchCampaign } = useQuery({ queryKey: ["campaign", id], queryFn: async () => { const res = await apiClient.get(`/campaigns/${id}`); return res.data; }, enabled: Boolean(id), }); const { data: sequences = [], refetch: refetchSequences } = useQuery({ queryKey: ["campaign-sequences", id], queryFn: async () => { const res = await apiClient.get(`/campaigns/${id}/sequences`); return res.data; }, enabled: Boolean(id), }); const { data: campaignLeads = [], refetch: refetchLeads } = useQuery({ queryKey: ["campaign-leads", id], queryFn: async () => { const res = await apiClient.get(`/campaigns/${id}/leads`); return res.data; }, enabled: Boolean(id), }); const { data: assignedAccounts = [], refetch: refetchAccounts } = useQuery({ queryKey: ["campaign-accounts", id], queryFn: async () => { const res = await apiClient.get(`/campaigns/${id}/email-accounts`); return res.data; }, enabled: Boolean(id), }); const { data: allEmailAccounts = [] } = useQuery({ queryKey: ["email-accounts"], queryFn: async () => { const res = await apiClient.get("/email-accounts"); return res.data; }, }); const { data: analyticsHistory = [] } = useQuery({ queryKey: ["campaign-analytics-date", id], queryFn: async () => { const res = await apiClient.get(`/campaigns/${id}/analytics-by-date`); return res.data; }, enabled: Boolean(id), }); // Save sequences mutation const saveSequencesMutation = useMutation({ mutationFn: async (updatedSequences: any[]) => { const res = await apiClient.post(`/campaigns/${id}/sequences`, { sequences: updatedSequences }); return res.data; }, onSuccess: () => { toast.success("Sequence saved successfully"); refetchSequences(); queryClient.invalidateQueries({ queryKey: ["campaign", id] }); }, onError: (err: any) => { toast.error(err.response?.data?.error || "Failed to save sequences"); }, }); const addSequenceStep = () => { const nextSeqNum = (sequences.length || 0) + 1; const newStep = { id: "seq_" + Math.random().toString(36).substring(2, 7), seq_number: nextSeqNum, delay_days: nextSeqNum === 1 ? 0 : 3, subject: `Follow-up #${nextSeqNum - 1}: Re: {{company_name}}`, email_body: `

Hi {{first_name}},

Wanted to quickly circle back on my earlier note. Any thoughts?

`, variant_distribution_percentage: 100, variants: [], }; saveSequencesMutation.mutate([...sequences, newStep]); }; const removeSequenceStep = (stepIndex: number) => { const filtered = sequences.filter((_: any, idx: number) => idx !== stepIndex); saveSequencesMutation.mutate(filtered); }; const addVariantToStep = (stepIndex: number) => { const updated = [...sequences]; const target = updated[stepIndex]; if (!target.variants) target.variants = []; target.variant_distribution_percentage = 50; target.variants.push({ id: "var_" + Math.random().toString(36).substring(2, 7), variant_label: `Variant ${String.fromCharCode(66 + target.variants.length)}`, subject: `Alternative: ${target.subject}`, email_body: `

Hey {{first_name}}, short follow up regarding {{company_name}}.

`, }); saveSequencesMutation.mutate(updated); }; const addAccountToCampaign = async (accountId: string) => { try { await apiClient.post(`/campaigns/${id}/email-accounts`, { email_account_id: accountId }); toast.success("Mailbox assigned to campaign"); refetchAccounts(); } catch (err: any) { toast.error("Failed to assign account"); } }; const removeAccountFromCampaign = async (accountId: string) => { try { await apiClient.delete(`/campaigns/${id}/email-accounts/${accountId}`); toast.success("Mailbox removed from campaign"); refetchAccounts(); } catch (err: any) { toast.error("Failed to remove account"); } }; if (isLoading) { return (
); } if (isError || !campaign) { return (

Campaign unavailable

We couldn’t load this campaign. Try again or return to the campaign list.

Back to campaigns
); } return (
{/* Back Navigation and Header */}
Back to all campaigns

{campaign.name}

Timezone: {campaign.timezone} • Schedule: {campaign.start_schedule} - {campaign.end_schedule}

{/* Tabs Bar */}
{[ { key: "overview", label: "Overview", icon: BarChart2 }, { key: "sequences", label: `Sequences (${sequences.length})`, icon: Layers }, { key: "accounts", label: `Email Accounts (${assignedAccounts.length})`, icon: Mail }, { key: "leads", label: `Leads (${campaignLeads.length})`, icon: Users }, { key: "analytics", label: "Analytics", icon: BarChart2 }, { key: "settings", label: "Settings", icon: SettingsIcon }, ].map((tab) => ( ))}
{/* TAB 1: OVERVIEW */} {activeTab === "overview" && (
{/* 14-Day Trajectory */}

Engagement Trajectory (Last 14 Days)

)} {/* TAB 2: SEQUENCES WITH A/B VARIANTS */} {activeTab === "sequences" && (

Multi-Step Sequence Flow

Set delay days, customize subject lines, and configure A/B variants per email step.

{sequences.map((step: any, stepIndex: number) => { const currentVariantTab = selectedVariantTab[step.id] || 0; const hasVariants = step.variants && step.variants.length > 0; return (
{/* Step Header */}
{step.seq_number}
{step.seq_number === 1 ? "Initial Outreach (Step 1)" : `Follow-up #${step.seq_number - 1}`}
Delay: { const updated = [...sequences]; updated[stepIndex].delay_days = Number(e.target.value); saveSequencesMutation.mutate(updated); }} className="w-12 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 text-xs text-zinc-100" /> days after previous email
{/* Step Body & Variant Tabs */}
{hasVariants && (
{step.variants.map((v: any, vIdx: number) => ( ))}
Split Traffic: 50% / 50%
)} {/* Active Variant Editor */} {currentVariantTab === 0 ? (
{ const updated = [...sequences]; updated[stepIndex].subject = e.target.value; saveSequencesMutation.mutate(updated); }} className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-xs text-zinc-100 focus:outline-none focus:border-indigo-500" />
{ const updated = [...sequences]; updated[stepIndex].email_body = html; saveSequencesMutation.mutate(updated); }} />
) : (
{ const updated = [...sequences]; updated[stepIndex].variants[currentVariantTab - 1].subject = e.target.value; saveSequencesMutation.mutate(updated); }} className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-xs text-zinc-100 focus:outline-none focus:border-indigo-500" />
{ const updated = [...sequences]; updated[stepIndex].variants[currentVariantTab - 1].email_body = html; saveSequencesMutation.mutate(updated); }} />
)}
); })}
)} {/* TAB 3: EMAIL ACCOUNTS ASSIGNMENT */} {activeTab === "accounts" && (

Assigned Sending Inboxes

Emails will be rotated across these connected mailboxes to preserve deliverability.

{assignedAccounts.map((acc: any) => (
{acc.from_name}
{acc.from_email}
))} {assignedAccounts.length === 0 && (
No email accounts assigned yet. Select from available mailboxes below.
)}

Available Mailboxes

{allEmailAccounts .filter((a: any) => !assignedAccounts.some((aa: any) => aa.id === a.id)) .map((acc: any) => (
{acc.from_email}
Provider: {acc.provider} • Daily limit: {acc.daily_limit} emails
))}
)} {/* TAB 4: LEADS */} {activeTab === "leads" && (
Showing {campaignLeads.length} leads in this campaign.
{campaignLeads.map((lead: any) => ( ))}
Lead Company Status Opens Clicks
{lead.first_name} {lead.last_name}
{lead.email}
{lead.company_name} {lead.open_count || 0} {lead.click_count || 0}
)} {/* TAB 5: ANALYTICS */} {activeTab === "analytics" && (
Open Rate
56.2%
Above industry avg (28%)
Reply Rate
6.4%
Strong positive intent
Bounce Rate
0.6%
Safe zone (< 2%)
)} {/* TAB 6: SETTINGS */} {activeTab === "settings" && (

Campaign Parameters

)} {/* Add Lead Slide-Over */} setIsAddLeadOpen(false)} title="Add Lead to Campaign" subtitle="Upload prospect details to be enrolled into this sequence." >
{ e.preventDefault(); const form = e.target as HTMLFormElement; const newLead = { first_name: (form.elements.namedItem("first_name") as HTMLInputElement).value, last_name: (form.elements.namedItem("last_name") as HTMLInputElement).value, email: (form.elements.namedItem("email") as HTMLInputElement).value, company_name: (form.elements.namedItem("company_name") as HTMLInputElement).value, website: (form.elements.namedItem("website") as HTMLInputElement).value, status: "INTERESTED", }; try { await apiClient.post(`/campaigns/${id}/leads`, newLead); toast.success("Lead enrolled in campaign"); setIsAddLeadOpen(false); refetchLeads(); } catch (err: any) { toast.error("Failed to add lead"); } }} className="space-y-4 text-xs" >
); }