sitcod3 commited on
Commit
ffb0c52
·
verified ·
1 Parent(s): 95bd9da

Upload src/pages/CampaignDetail.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pages/CampaignDetail.tsx +727 -0
src/pages/CampaignDetail.tsx ADDED
@@ -0,0 +1,727 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient } from "../lib/apiClient";
3
+ import { useParams, Link } from "react-router-dom";
4
+ import {
5
+ ArrowLeft,
6
+ Megaphone,
7
+ Layers,
8
+ Mail,
9
+ Users,
10
+ BarChart2,
11
+ Settings as SettingsIcon,
12
+ Plus,
13
+ Trash2,
14
+ Play,
15
+ Pause,
16
+ Clock,
17
+ CheckCircle2,
18
+ Sparkles,
19
+ Download,
20
+ Send,
21
+ Split,
22
+ RefreshCw
23
+ } from "lucide-react";
24
+ import { useState } from "react";
25
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
26
+ import StatusBadge from "../components/StatusBadge";
27
+ import StatCard from "../components/StatCard";
28
+ import RichTextEditor from "../components/RichTextEditor";
29
+ import FormDrawer from "../components/FormDrawer";
30
+ import CsvExportButton from "../components/CsvExportButton";
31
+ import toast from "react-hot-toast";
32
+
33
+ export default function CampaignDetail() {
34
+ const { id } = useParams<{ id: string }>();
35
+ const queryClient = useQueryClient();
36
+ const [activeTab, setActiveTab] = useState<"overview" | "sequences" | "accounts" | "leads" | "analytics" | "settings">("overview");
37
+ const [isAddLeadOpen, setIsAddLeadOpen] = useState(false);
38
+ const [selectedVariantTab, setSelectedVariantTab] = useState<Record<string, number>>({});
39
+
40
+ const { data: campaign, isLoading, isError, refetch: refetchCampaign } = useQuery({
41
+ queryKey: ["campaign", id],
42
+ queryFn: async () => {
43
+ const res = await apiClient.get(`/campaigns/${id}`);
44
+ return res.data;
45
+ },
46
+ enabled: Boolean(id),
47
+ });
48
+
49
+ const { data: sequences = [], refetch: refetchSequences } = useQuery({
50
+ queryKey: ["campaign-sequences", id],
51
+ queryFn: async () => {
52
+ const res = await apiClient.get(`/campaigns/${id}/sequences`);
53
+ return res.data;
54
+ },
55
+ enabled: Boolean(id),
56
+ });
57
+
58
+ const { data: campaignLeads = [], refetch: refetchLeads } = useQuery({
59
+ queryKey: ["campaign-leads", id],
60
+ queryFn: async () => {
61
+ const res = await apiClient.get(`/campaigns/${id}/leads`);
62
+ return res.data;
63
+ },
64
+ enabled: Boolean(id),
65
+ });
66
+
67
+ const { data: assignedAccounts = [], refetch: refetchAccounts } = useQuery({
68
+ queryKey: ["campaign-accounts", id],
69
+ queryFn: async () => {
70
+ const res = await apiClient.get(`/campaigns/${id}/email-accounts`);
71
+ return res.data;
72
+ },
73
+ enabled: Boolean(id),
74
+ });
75
+
76
+ const { data: allEmailAccounts = [] } = useQuery({
77
+ queryKey: ["email-accounts"],
78
+ queryFn: async () => {
79
+ const res = await apiClient.get("/email-accounts");
80
+ return res.data;
81
+ },
82
+ });
83
+
84
+ const { data: analyticsHistory = [] } = useQuery({
85
+ queryKey: ["campaign-analytics-date", id],
86
+ queryFn: async () => {
87
+ const res = await apiClient.get(`/campaigns/${id}/analytics-by-date`);
88
+ return res.data;
89
+ },
90
+ enabled: Boolean(id),
91
+ });
92
+
93
+ // Save sequences mutation
94
+ const saveSequencesMutation = useMutation({
95
+ mutationFn: async (updatedSequences: any[]) => {
96
+ const res = await apiClient.post(`/campaigns/${id}/sequences`, { sequences: updatedSequences });
97
+ return res.data;
98
+ },
99
+ onSuccess: () => {
100
+ toast.success("Sequence saved successfully");
101
+ refetchSequences();
102
+ queryClient.invalidateQueries({ queryKey: ["campaign", id] });
103
+ },
104
+ onError: (err: any) => {
105
+ toast.error(err.response?.data?.error || "Failed to save sequences");
106
+ },
107
+ });
108
+
109
+ const addSequenceStep = () => {
110
+ const nextSeqNum = (sequences.length || 0) + 1;
111
+ const newStep = {
112
+ id: "seq_" + Math.random().toString(36).substring(2, 7),
113
+ seq_number: nextSeqNum,
114
+ delay_days: nextSeqNum === 1 ? 0 : 3,
115
+ subject: `Follow-up #${nextSeqNum - 1}: Re: {{company_name}}`,
116
+ email_body: `<p>Hi {{first_name}},</p><p>Wanted to quickly circle back on my earlier note. Any thoughts?</p>`,
117
+ variant_distribution_percentage: 100,
118
+ variants: [],
119
+ };
120
+ saveSequencesMutation.mutate([...sequences, newStep]);
121
+ };
122
+
123
+ const removeSequenceStep = (stepIndex: number) => {
124
+ const filtered = sequences.filter((_: any, idx: number) => idx !== stepIndex);
125
+ saveSequencesMutation.mutate(filtered);
126
+ };
127
+
128
+ const addVariantToStep = (stepIndex: number) => {
129
+ const updated = [...sequences];
130
+ const target = updated[stepIndex];
131
+ if (!target.variants) target.variants = [];
132
+ target.variant_distribution_percentage = 50;
133
+ target.variants.push({
134
+ id: "var_" + Math.random().toString(36).substring(2, 7),
135
+ variant_label: `Variant ${String.fromCharCode(66 + target.variants.length)}`,
136
+ subject: `Alternative: ${target.subject}`,
137
+ email_body: `<p>Hey {{first_name}}, short follow up regarding {{company_name}}.</p>`,
138
+ });
139
+ saveSequencesMutation.mutate(updated);
140
+ };
141
+
142
+ const addAccountToCampaign = async (accountId: string) => {
143
+ try {
144
+ await apiClient.post(`/campaigns/${id}/email-accounts`, { email_account_id: accountId });
145
+ toast.success("Mailbox assigned to campaign");
146
+ refetchAccounts();
147
+ } catch (err: any) {
148
+ toast.error("Failed to assign account");
149
+ }
150
+ };
151
+
152
+ const removeAccountFromCampaign = async (accountId: string) => {
153
+ try {
154
+ await apiClient.delete(`/campaigns/${id}/email-accounts/${accountId}`);
155
+ toast.success("Mailbox removed from campaign");
156
+ refetchAccounts();
157
+ } catch (err: any) {
158
+ toast.error("Failed to remove account");
159
+ }
160
+ };
161
+
162
+ if (isLoading) {
163
+ return (
164
+ <div className="p-12 flex justify-center items-center">
165
+ <div className="w-8 h-8 rounded-full border-2 border-indigo-500 border-t-transparent animate-spin" />
166
+ </div>
167
+ );
168
+ }
169
+
170
+ if (isError || !campaign) {
171
+ return (
172
+ <div className="p-12 flex flex-col items-center justify-center gap-3 text-center">
173
+ <h1 className="text-lg font-semibold text-zinc-100">Campaign unavailable</h1>
174
+ <p className="text-sm text-zinc-400">We couldn’t load this campaign. Try again or return to the campaign list.</p>
175
+ <div className="flex items-center gap-2">
176
+ <button type="button" onClick={() => refetchCampaign()} className="px-3 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium rounded-lg inline-flex items-center gap-2">
177
+ <RefreshCw className="w-3.5 h-3.5" /> Retry
178
+ </button>
179
+ <Link to="/campaigns" className="px-3 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-xs font-medium rounded-lg">
180
+ Back to campaigns
181
+ </Link>
182
+ </div>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ return (
188
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
189
+ {/* Back Navigation and Header */}
190
+ <div className="space-y-3">
191
+ <Link
192
+ to="/campaigns"
193
+ className="inline-flex items-center gap-1.5 text-xs text-zinc-400 hover:text-zinc-200 transition-colors"
194
+ >
195
+ <ArrowLeft className="w-3.5 h-3.5" /> Back to all campaigns
196
+ </Link>
197
+
198
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
199
+ <div>
200
+ <div className="flex items-center gap-3">
201
+ <h1 className="text-2xl font-bold text-zinc-100 tracking-tight">{campaign.name}</h1>
202
+ <StatusBadge status={campaign.status} />
203
+ </div>
204
+ <p className="text-xs text-zinc-400 mt-1">
205
+ Timezone: {campaign.timezone} • Schedule: {campaign.start_schedule} - {campaign.end_schedule}
206
+ </p>
207
+ </div>
208
+
209
+ <div className="flex items-center gap-2">
210
+ <button
211
+ onClick={() => setIsAddLeadOpen(true)}
212
+ className="px-3.5 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5"
213
+ >
214
+ <Plus className="w-3.5 h-3.5" /> Add Leads
215
+ </button>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ {/* Tabs Bar */}
221
+ <div className="border-b border-zinc-800 flex items-center gap-1 text-sm font-medium overflow-x-auto">
222
+ {[
223
+ { key: "overview", label: "Overview", icon: BarChart2 },
224
+ { key: "sequences", label: `Sequences (${sequences.length})`, icon: Layers },
225
+ { key: "accounts", label: `Email Accounts (${assignedAccounts.length})`, icon: Mail },
226
+ { key: "leads", label: `Leads (${campaignLeads.length})`, icon: Users },
227
+ { key: "analytics", label: "Analytics", icon: BarChart2 },
228
+ { key: "settings", label: "Settings", icon: SettingsIcon },
229
+ ].map((tab) => (
230
+ <button
231
+ key={tab.key}
232
+ onClick={() => setActiveTab(tab.key as any)}
233
+ className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors whitespace-nowrap ${
234
+ activeTab === tab.key
235
+ ? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
236
+ : "border-transparent text-zinc-400 hover:text-zinc-200 hover:border-zinc-700"
237
+ }`}
238
+ >
239
+ <tab.icon className="w-4 h-4" />
240
+ {tab.label}
241
+ </button>
242
+ ))}
243
+ </div>
244
+
245
+ {/* TAB 1: OVERVIEW */}
246
+ {activeTab === "overview" && (
247
+ <div className="space-y-6">
248
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
249
+ <StatCard
250
+ title="Leads in Sequence"
251
+ value={campaign.lead_count || 0}
252
+ icon={Users}
253
+ iconColor="teal"
254
+ />
255
+ <StatCard
256
+ title="Emails Sent"
257
+ value={campaign.sent_count || 0}
258
+ icon={Send}
259
+ iconColor="blue"
260
+ />
261
+ <StatCard
262
+ title="Unique Opens"
263
+ value={campaign.open_count || 0}
264
+ subtitle={`${((campaign.open_count / (campaign.sent_count || 1)) * 100).toFixed(1)}% Open Rate`}
265
+ icon={Mail}
266
+ iconColor="emerald"
267
+ />
268
+ <StatCard
269
+ title="Replies Received"
270
+ value={campaign.reply_count || 0}
271
+ subtitle={`${((campaign.reply_count / (campaign.sent_count || 1)) * 100).toFixed(1)}% Reply Rate`}
272
+ icon={CheckCircle2}
273
+ iconColor="purple"
274
+ />
275
+ </div>
276
+
277
+ {/* 14-Day Trajectory */}
278
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-5 shadow-sm">
279
+ <h3 className="text-sm font-semibold text-zinc-100 mb-4">
280
+ Engagement Trajectory (Last 14 Days)
281
+ </h3>
282
+ <div className="h-[260px] w-full">
283
+ <ResponsiveContainer width="100%" height="100%">
284
+ <LineChart data={analyticsHistory} margin={{ top: 5, right: 10, bottom: 5, left: -15 }}>
285
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--nt-border-subtle)" vertical={false} />
286
+ <XAxis dataKey="date" stroke="var(--nt-text-muted)" fontSize={11} tickLine={false} axisLine={false} />
287
+ <YAxis stroke="var(--nt-text-muted)" fontSize={11} tickLine={false} axisLine={false} />
288
+ <Tooltip
289
+ contentStyle={{ backgroundColor: "var(--nt-bg-root)", borderColor: "var(--nt-border-accent)", borderRadius: "8px", fontSize: "12px" }}
290
+ />
291
+ <Line type="monotone" dataKey="sent" stroke="var(--nt-deep-teal-600)" strokeWidth={2} name="Sent" dot={false} />
292
+ <Line type="monotone" dataKey="opens" stroke="var(--nt-deep-teal-500)" strokeWidth={2} name="Opens" dot={false} />
293
+ <Line type="monotone" dataKey="replies" stroke="var(--nt-terracotta-500)" strokeWidth={2} name="Replies" dot={false} />
294
+ </LineChart>
295
+ </ResponsiveContainer>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ )}
300
+
301
+ {/* TAB 2: SEQUENCES WITH A/B VARIANTS */}
302
+ {activeTab === "sequences" && (
303
+ <div className="space-y-6">
304
+ <div className="flex items-center justify-between">
305
+ <div>
306
+ <h3 className="text-base font-semibold text-zinc-100">Multi-Step Sequence Flow</h3>
307
+ <p className="text-xs text-zinc-400 mt-0.5">
308
+ Set delay days, customize subject lines, and configure A/B variants per email step.
309
+ </p>
310
+ </div>
311
+ <button
312
+ onClick={addSequenceStep}
313
+ className="px-3.5 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5"
314
+ >
315
+ <Plus className="w-3.5 h-3.5" /> Add Email Step
316
+ </button>
317
+ </div>
318
+
319
+ <div className="space-y-4">
320
+ {sequences.map((step: any, stepIndex: number) => {
321
+ const currentVariantTab = selectedVariantTab[step.id] || 0;
322
+ const hasVariants = step.variants && step.variants.length > 0;
323
+
324
+ return (
325
+ <div key={step.id || stepIndex} className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden shadow-sm">
326
+ {/* Step Header */}
327
+ <div className="px-5 py-3.5 bg-zinc-950 border-b border-zinc-800 flex items-center justify-between">
328
+ <div className="flex items-center gap-3">
329
+ <span className="w-6 h-6 rounded-full bg-indigo-500/20 text-indigo-400 font-bold text-xs flex items-center justify-center border border-indigo-500/30">
330
+ {step.seq_number}
331
+ </span>
332
+ <div>
333
+ <span className="text-xs font-semibold text-zinc-200">
334
+ {step.seq_number === 1 ? "Initial Outreach (Step 1)" : `Follow-up #${step.seq_number - 1}`}
335
+ </span>
336
+ <div className="flex items-center gap-2 text-[11px] text-zinc-400 mt-0.5">
337
+ <Clock className="w-3 h-3 text-zinc-500" />
338
+ <span>Delay:</span>
339
+ <input
340
+ type="number"
341
+ min="0"
342
+ value={step.delay_days}
343
+ onChange={(e) => {
344
+ const updated = [...sequences];
345
+ updated[stepIndex].delay_days = Number(e.target.value);
346
+ saveSequencesMutation.mutate(updated);
347
+ }}
348
+ className="w-12 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 text-xs text-zinc-100"
349
+ />
350
+ <span>days after previous email</span>
351
+ </div>
352
+ </div>
353
+ </div>
354
+
355
+ <div className="flex items-center gap-2">
356
+ <button
357
+ type="button"
358
+ onClick={() => addVariantToStep(stepIndex)}
359
+ className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs font-medium rounded-md transition-colors flex items-center gap-1"
360
+ >
361
+ <Split className="w-3.5 h-3.5 text-indigo-400" /> + A/B Variant
362
+ </button>
363
+ <button
364
+ type="button"
365
+ onClick={() => removeSequenceStep(stepIndex)}
366
+ className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors"
367
+ >
368
+ <Trash2 className="w-4 h-4" />
369
+ </button>
370
+ </div>
371
+ </div>
372
+
373
+ {/* Step Body & Variant Tabs */}
374
+ <div className="p-5 space-y-4">
375
+ {hasVariants && (
376
+ <div className="flex items-center justify-between border-b border-zinc-800 pb-3">
377
+ <div className="flex items-center gap-2">
378
+ <button
379
+ type="button"
380
+ onClick={() => setSelectedVariantTab((prev) => ({ ...prev, [step.id]: 0 }))}
381
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
382
+ currentVariantTab === 0 ? "bg-indigo-600 text-white" : "bg-zinc-800 text-zinc-400"
383
+ }`}
384
+ >
385
+ Variant A (Default)
386
+ </button>
387
+ {step.variants.map((v: any, vIdx: number) => (
388
+ <button
389
+ key={v.id || vIdx}
390
+ type="button"
391
+ onClick={() => setSelectedVariantTab((prev) => ({ ...prev, [step.id]: vIdx + 1 }))}
392
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
393
+ currentVariantTab === vIdx + 1 ? "bg-indigo-600 text-white" : "bg-zinc-800 text-zinc-400"
394
+ }`}
395
+ >
396
+ {v.variant_label || `Variant ${String.fromCharCode(66 + vIdx)}`}
397
+ </button>
398
+ ))}
399
+ </div>
400
+
401
+ <div className="text-xs text-zinc-400 font-mono">
402
+ Split Traffic: 50% / 50%
403
+ </div>
404
+ </div>
405
+ )}
406
+
407
+ {/* Active Variant Editor */}
408
+ {currentVariantTab === 0 ? (
409
+ <div className="space-y-3">
410
+ <div>
411
+ <label className="block text-xs font-medium text-zinc-400 mb-1">
412
+ Subject Line (Variant A)
413
+ </label>
414
+ <input
415
+ type="text"
416
+ value={step.subject}
417
+ onChange={(e) => {
418
+ const updated = [...sequences];
419
+ updated[stepIndex].subject = e.target.value;
420
+ saveSequencesMutation.mutate(updated);
421
+ }}
422
+ 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"
423
+ />
424
+ </div>
425
+
426
+ <div>
427
+ <label className="block text-xs font-medium text-zinc-400 mb-1">
428
+ Email Body (Variant A)
429
+ </label>
430
+ <RichTextEditor
431
+ content={step.email_body}
432
+ onChange={(html) => {
433
+ const updated = [...sequences];
434
+ updated[stepIndex].email_body = html;
435
+ saveSequencesMutation.mutate(updated);
436
+ }}
437
+ />
438
+ </div>
439
+ </div>
440
+ ) : (
441
+ <div className="space-y-3">
442
+ <div>
443
+ <label className="block text-xs font-medium text-zinc-400 mb-1">
444
+ Subject Line ({step.variants[currentVariantTab - 1]?.variant_label})
445
+ </label>
446
+ <input
447
+ type="text"
448
+ value={step.variants[currentVariantTab - 1]?.subject || ""}
449
+ onChange={(e) => {
450
+ const updated = [...sequences];
451
+ updated[stepIndex].variants[currentVariantTab - 1].subject = e.target.value;
452
+ saveSequencesMutation.mutate(updated);
453
+ }}
454
+ 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"
455
+ />
456
+ </div>
457
+
458
+ <div>
459
+ <label className="block text-xs font-medium text-zinc-400 mb-1">
460
+ Email Body ({step.variants[currentVariantTab - 1]?.variant_label})
461
+ </label>
462
+ <RichTextEditor
463
+ content={step.variants[currentVariantTab - 1]?.email_body || ""}
464
+ onChange={(html) => {
465
+ const updated = [...sequences];
466
+ updated[stepIndex].variants[currentVariantTab - 1].email_body = html;
467
+ saveSequencesMutation.mutate(updated);
468
+ }}
469
+ />
470
+ </div>
471
+ </div>
472
+ )}
473
+ </div>
474
+ </div>
475
+ );
476
+ })}
477
+ </div>
478
+ </div>
479
+ )}
480
+
481
+ {/* TAB 3: EMAIL ACCOUNTS ASSIGNMENT */}
482
+ {activeTab === "accounts" && (
483
+ <div className="space-y-6">
484
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-5">
485
+ <h3 className="text-base font-semibold text-zinc-100 mb-1">Assigned Sending Inboxes</h3>
486
+ <p className="text-xs text-zinc-400 mb-4">
487
+ Emails will be rotated across these connected mailboxes to preserve deliverability.
488
+ </p>
489
+
490
+ <div className="space-y-3">
491
+ {assignedAccounts.map((acc: any) => (
492
+ <div
493
+ key={acc.id}
494
+ className="flex items-center justify-between p-3 bg-zinc-950 border border-zinc-800 rounded-lg"
495
+ >
496
+ <div className="flex items-center gap-3">
497
+ <div className="p-2 bg-indigo-500/10 text-indigo-400 rounded-md">
498
+ <Mail className="w-4 h-4" />
499
+ </div>
500
+ <div>
501
+ <span className="text-xs font-semibold text-zinc-200">{acc.from_name}</span>
502
+ <div className="text-[11px] text-zinc-400">{acc.from_email}</div>
503
+ </div>
504
+ </div>
505
+
506
+ <div className="flex items-center gap-3">
507
+ <StatusBadge status={acc.status} size="sm" />
508
+ <button
509
+ onClick={() => removeAccountFromCampaign(acc.id)}
510
+ className="text-xs text-rose-400 hover:text-rose-300 font-medium"
511
+ >
512
+ Remove
513
+ </button>
514
+ </div>
515
+ </div>
516
+ ))}
517
+
518
+ {assignedAccounts.length === 0 && (
519
+ <div className="p-6 text-center text-xs text-zinc-500 bg-zinc-950/60 rounded-lg border border-dashed border-zinc-800">
520
+ No email accounts assigned yet. Select from available mailboxes below.
521
+ </div>
522
+ )}
523
+ </div>
524
+ </div>
525
+
526
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-5">
527
+ <h4 className="text-xs font-semibold uppercase tracking-wider text-zinc-400 mb-3">
528
+ Available Mailboxes
529
+ </h4>
530
+ <div className="space-y-2">
531
+ {allEmailAccounts
532
+ .filter((a: any) => !assignedAccounts.some((aa: any) => aa.id === a.id))
533
+ .map((acc: any) => (
534
+ <div
535
+ key={acc.id}
536
+ className="flex items-center justify-between p-3 bg-zinc-950/40 border border-zinc-800/80 rounded-lg"
537
+ >
538
+ <div>
539
+ <span className="text-xs font-medium text-zinc-200">{acc.from_email}</span>
540
+ <div className="text-[10px] text-zinc-500">
541
+ Provider: {acc.provider} • Daily limit: {acc.daily_limit} emails
542
+ </div>
543
+ </div>
544
+ <button
545
+ onClick={() => addAccountToCampaign(acc.id)}
546
+ className="px-3 py-1 bg-indigo-600/20 hover:bg-indigo-600 text-indigo-300 hover:text-white rounded text-xs font-medium transition-colors"
547
+ >
548
+ + Assign to Campaign
549
+ </button>
550
+ </div>
551
+ ))}
552
+ </div>
553
+ </div>
554
+ </div>
555
+ )}
556
+
557
+ {/* TAB 4: LEADS */}
558
+ {activeTab === "leads" && (
559
+ <div className="space-y-4">
560
+ <div className="flex items-center justify-between">
561
+ <div className="text-xs text-zinc-400">
562
+ Showing {campaignLeads.length} leads in this campaign.
563
+ </div>
564
+ <div className="flex items-center gap-2">
565
+ <CsvExportButton data={campaignLeads} filename={`campaign-${id}-leads.csv`} />
566
+ <button
567
+ onClick={() => setIsAddLeadOpen(true)}
568
+ className="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5"
569
+ >
570
+ <Plus className="w-3.5 h-3.5" /> Add Lead
571
+ </button>
572
+ </div>
573
+ </div>
574
+
575
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden">
576
+ <table className="w-full text-sm text-left">
577
+ <thead className="bg-zinc-950 border-b border-zinc-800 text-zinc-400 text-xs uppercase">
578
+ <tr>
579
+ <th className="px-5 py-3 font-semibold">Lead</th>
580
+ <th className="px-5 py-3 font-semibold">Company</th>
581
+ <th className="px-5 py-3 font-semibold">Status</th>
582
+ <th className="px-5 py-3 font-semibold">Opens</th>
583
+ <th className="px-5 py-3 font-semibold">Clicks</th>
584
+ </tr>
585
+ </thead>
586
+ <tbody className="divide-y divide-zinc-800">
587
+ {campaignLeads.map((lead: any) => (
588
+ <tr key={lead.id} className="hover:bg-zinc-800/30">
589
+ <td className="px-5 py-3.5">
590
+ <div className="font-medium text-zinc-200">
591
+ {lead.first_name} {lead.last_name}
592
+ </div>
593
+ <div className="text-xs text-zinc-400 font-mono">{lead.email}</div>
594
+ </td>
595
+ <td className="px-5 py-3.5 text-zinc-300">{lead.company_name}</td>
596
+ <td className="px-5 py-3.5">
597
+ <StatusBadge status={lead.status} size="sm" />
598
+ </td>
599
+ <td className="px-5 py-3.5 text-zinc-300 font-mono">{lead.open_count || 0}</td>
600
+ <td className="px-5 py-3.5 text-zinc-300 font-mono">{lead.click_count || 0}</td>
601
+ </tr>
602
+ ))}
603
+ </tbody>
604
+ </table>
605
+ </div>
606
+ </div>
607
+ )}
608
+
609
+ {/* TAB 5: ANALYTICS */}
610
+ {activeTab === "analytics" && (
611
+ <div className="space-y-6">
612
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
613
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
614
+ <span className="text-xs text-zinc-400">Open Rate</span>
615
+ <div className="text-2xl font-bold text-emerald-400 mt-1">56.2%</div>
616
+ <span className="text-[11px] text-zinc-500">Above industry avg (28%)</span>
617
+ </div>
618
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
619
+ <span className="text-xs text-zinc-400">Reply Rate</span>
620
+ <div className="text-2xl font-bold text-purple-400 mt-1">6.4%</div>
621
+ <span className="text-[11px] text-zinc-500">Strong positive intent</span>
622
+ </div>
623
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
624
+ <span className="text-xs text-zinc-400">Bounce Rate</span>
625
+ <div className="text-2xl font-bold text-rose-400 mt-1">0.6%</div>
626
+ <span className="text-[11px] text-zinc-500">Safe zone (&lt; 2%)</span>
627
+ </div>
628
+ </div>
629
+ </div>
630
+ )}
631
+
632
+ {/* TAB 6: SETTINGS */}
633
+ {activeTab === "settings" && (
634
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 max-w-2xl">
635
+ <h3 className="text-base font-semibold text-zinc-100 mb-4">Campaign Parameters</h3>
636
+ <div className="space-y-4 text-xs">
637
+ <div>
638
+ <label className="block text-zinc-400 font-medium mb-1">Timezone</label>
639
+ <input
640
+ disabled
641
+ value={campaign.timezone}
642
+ className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-zinc-300"
643
+ />
644
+ </div>
645
+ <div>
646
+ <label className="block text-zinc-400 font-medium mb-1">Unsubscribe Footer</label>
647
+ <input
648
+ disabled
649
+ value={campaign.unsubscribe_text}
650
+ className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-zinc-300"
651
+ />
652
+ </div>
653
+ </div>
654
+ </div>
655
+ )}
656
+
657
+ {/* Add Lead Slide-Over */}
658
+ <FormDrawer
659
+ isOpen={isAddLeadOpen}
660
+ onClose={() => setIsAddLeadOpen(false)}
661
+ title="Add Lead to Campaign"
662
+ subtitle="Upload prospect details to be enrolled into this sequence."
663
+ >
664
+ <form
665
+ onSubmit={async (e) => {
666
+ e.preventDefault();
667
+ const form = e.target as HTMLFormElement;
668
+ const newLead = {
669
+ first_name: (form.elements.namedItem("first_name") as HTMLInputElement).value,
670
+ last_name: (form.elements.namedItem("last_name") as HTMLInputElement).value,
671
+ email: (form.elements.namedItem("email") as HTMLInputElement).value,
672
+ company_name: (form.elements.namedItem("company_name") as HTMLInputElement).value,
673
+ website: (form.elements.namedItem("website") as HTMLInputElement).value,
674
+ status: "INTERESTED",
675
+ };
676
+
677
+ try {
678
+ await apiClient.post(`/campaigns/${id}/leads`, newLead);
679
+ toast.success("Lead enrolled in campaign");
680
+ setIsAddLeadOpen(false);
681
+ refetchLeads();
682
+ } catch (err: any) {
683
+ toast.error("Failed to add lead");
684
+ }
685
+ }}
686
+ className="space-y-4 text-xs"
687
+ >
688
+ <div>
689
+ <label className="block font-medium text-zinc-300 mb-1">First Name *</label>
690
+ <input name="first_name" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
691
+ </div>
692
+ <div>
693
+ <label className="block font-medium text-zinc-300 mb-1">Last Name</label>
694
+ <input name="last_name" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
695
+ </div>
696
+ <div>
697
+ <label className="block font-medium text-zinc-300 mb-1">Email *</label>
698
+ <input type="email" name="email" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
699
+ </div>
700
+ <div>
701
+ <label className="block font-medium text-zinc-300 mb-1">Company Name</label>
702
+ <input name="company_name" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
703
+ </div>
704
+ <div>
705
+ <label className="block font-medium text-zinc-300 mb-1">Website</label>
706
+ <input name="website" placeholder="https://" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
707
+ </div>
708
+ <div className="pt-4 flex justify-end gap-2">
709
+ <button
710
+ type="button"
711
+ onClick={() => setIsAddLeadOpen(false)}
712
+ className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
713
+ >
714
+ Cancel
715
+ </button>
716
+ <button
717
+ type="submit"
718
+ className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium"
719
+ >
720
+ Enroll Lead
721
+ </button>
722
+ </div>
723
+ </form>
724
+ </FormDrawer>
725
+ </div>
726
+ );
727
+ }