"use client" import { useState, useEffect, useCallback, useRef } from "react" import { Loader2, MessageCircle, Mail, Smartphone, Check, X, Clock, ChevronDown, Send, Zap, AlertTriangle, Plus, Trash2, Pencil, RotateCcw, CheckCheck } from "lucide-react" import Link from "next/link" import { resolvePreview, TEMPLATE_VARIABLES, STEP_META, STRATEGY_PRESETS } from "@/lib/templates" /** * /dashboard/automations — THE MESSAGE DESIGN STUDIO * * This is not a monitoring dashboard. This is where Aaisha designs * the messages her donors receive. She sees exactly what Ahmed * sees on his phone — a WhatsApp-native mockup with green bubbles, * blue ticks, and her charity's name in the header. * * Layout: * ┌─────────────────────────────────────────────────────────────┐ * │ STEP TIMELINE (4 tabs across the top) │ * ├─────────────────────┬───────────────────────────────────────┤ * │ PHONE MOCKUP │ EDITOR │ * │ (WhatsApp-native) │ Channel tabs + textarea + variables │ * │ │ A/B variant toggle │ * │ │ Save / Test / Reset │ * ├─────────────────────┴───────────────────────────────────────┤ * │ CHANNEL STRATEGY (matrix + presets) │ * ├─────────────────────────────────────────────────────────────┤ * │ STATS + FEED (condensed at bottom) │ * └─────────────────────────────────────────────────────────────┘ */ // ─── Types ─────────────────────────────────────────────────── interface Template { id: string; step: number; channel: string; variant: string name: string; subject: string | null; body: string isActive: boolean; splitPercent: number sentCount: number; convertedCount: number } interface Config { isActive: boolean; step1Delay: number; step2Delay: number; step3Delay: number strategy: string; channelMatrix: Record | null } interface ChannelStatus { whatsapp: boolean; email: { provider: string; fromAddress: string } | null; sms: { provider: string; fromNumber: string } | null } interface ChannelStats { whatsapp: { sent: number; failed: number }; email: { sent: number; failed: number }; sms: { sent: number; failed: number }; total: number; deliveryRate: number } interface MessageEntry { id: string; channel: string; messageType: string; donorName: string | null; success: boolean; error: string | null; createdAt: string } interface PendingEntry { id: string; donorName: string | null; amountPence: number; step: number; channel: string; scheduledAt: string } const CHANNELS = ["whatsapp", "email", "sms"] as const type Channel = typeof CHANNELS[number] const CH_META: Record = { whatsapp: { icon: MessageCircle, color: "#25D366", label: "WhatsApp" }, email: { icon: Mail, color: "#1E40AF", label: "Email" }, sms: { icon: Smartphone, color: "#F59E0B", label: "SMS" }, } // ─── Page ──────────────────────────────────────────────────── export default function AutomationsPage() { const [loading, setLoading] = useState(true) const [templates, setTemplates] = useState([]) const [config, setConfig] = useState(null) const [channels, setChannels] = useState(null) const [stats, setStats] = useState(null) const [history, setHistory] = useState([]) const [pending, setPending] = useState([]) // Current editor state const [activeStep, setActiveStep] = useState(0) const [activeChannel, setActiveChannel] = useState("whatsapp") const [activeVariant, setActiveVariant] = useState("A") const [editBody, setEditBody] = useState("") const [editSubject, setEditSubject] = useState("") const [editName, setEditName] = useState("") const [dirty, setDirty] = useState(false) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [showStrategy, setShowStrategy] = useState(false) const [showFeed, setShowFeed] = useState(false) const editorRef = useRef(null) const load = useCallback(async () => { try { const res = await fetch("/api/automations") const data = await res.json() if (data.templates) setTemplates(data.templates) if (data.config) setConfig(data.config) if (data.channels) setChannels(data.channels) if (data.stats) setStats(data.stats) if (data.history) setHistory(data.history) if (data.pending) setPending(data.pending) } catch { /* */ } setLoading(false) }, []) useEffect(() => { load() }, [load]) // Sync editor when selection changes useEffect(() => { const tpl = templates.find(t => t.step === activeStep && t.channel === activeChannel && t.variant === activeVariant) if (tpl) { setEditBody(tpl.body) setEditSubject(tpl.subject || "") setEditName(tpl.name) setDirty(false) } else { setEditBody("") setEditSubject("") setEditName(STEP_META[activeStep]?.label || "") setDirty(false) } }, [activeStep, activeChannel, activeVariant, templates]) const currentTemplate = templates.find(t => t.step === activeStep && t.channel === activeChannel && t.variant === activeVariant) const variantB = templates.find(t => t.step === activeStep && t.channel === activeChannel && t.variant === "B") const hasVariantB = !!variantB const activeChannels = [ channels?.whatsapp ? "whatsapp" : null, channels?.email ? "email" : null, channels?.sms ? "sms" : null, ].filter(Boolean) as Channel[] // ─── Actions ────────────────────────────── const saveTemplate = async () => { setSaving(true) try { await fetch("/api/automations", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ templates: [{ step: activeStep, channel: activeChannel, variant: activeVariant, name: editName, subject: activeChannel === "email" ? editSubject : null, body: editBody, splitPercent: hasVariantB ? 50 : 100, }], }), }) setSaved(true) setDirty(false) setTimeout(() => setSaved(false), 2000) await load() } catch { /* */ } setSaving(false) } const createVariantB = async () => { const tplA = templates.find(t => t.step === activeStep && t.channel === activeChannel && t.variant === "A") if (!tplA) return setSaving(true) try { await fetch("/api/automations", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ templates: [ { step: activeStep, channel: activeChannel, variant: "A", name: tplA.name, subject: tplA.subject, body: tplA.body, splitPercent: 50 }, { step: activeStep, channel: activeChannel, variant: "B", name: tplA.name + " (B)", subject: tplA.subject, body: tplA.body, splitPercent: 50 }, ], }), }) setActiveVariant("B") await load() } catch { /* */ } setSaving(false) } const deleteVariantB = async () => { setSaving(true) try { await fetch("/api/automations", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step: activeStep, channel: activeChannel, variant: "B" }), }) setActiveVariant("A") await load() } catch { /* */ } setSaving(false) } const saveStrategy = async (strategy: string, matrix: Record) => { try { await fetch("/api/automations", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ config: { strategy, channelMatrix: matrix } }), }) await load() } catch { /* */ } } const saveTiming = async (key: string, value: number) => { try { await fetch("/api/automations", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ config: { [key]: value } }), }) await load() } catch { /* */ } } const insertVariable = (key: string) => { const el = editorRef.current if (!el) return const start = el.selectionStart const end = el.selectionEnd const insert = `{{${key}}}` const newBody = editBody.slice(0, start) + insert + editBody.slice(end) setEditBody(newBody) setDirty(true) setTimeout(() => { el.focus() el.setSelectionRange(start + insert.length, start + insert.length) }, 0) } // ─── Render ─────────────────────────────── if (loading) return
const noChannels = activeChannels.length === 0 return (
{/* ── Header ── */}

Message design studio

Automations

{stats && stats.total > 0 && (

{stats.total}

messages this week · {stats.deliveryRate}% delivered

)}
{/* ── No channels warning ── */} {noChannels && (

No channels connected yet

Connect WhatsApp in Settings to start sending messages. You can design your messages now and they'll start sending once connected.

)} {/* ── Step Timeline ── */}
{STEP_META.map((s, i) => { const isActive = activeStep === i const delay = i === 0 ? "Instant" : i === 1 ? `Day ${config?.step1Delay || 2}` : i === 2 ? `Day ${config?.step2Delay || 7}` : `Day ${config?.step3Delay || 14}` const tplCount = templates.filter(t => t.step === i).length return ( ) })}
{/* ── Main: Phone + Editor ── */}
{/* ── LEFT: Phone Mockup ── */}
{/* Step timing (editable) */} {activeStep > 0 && (
Send after pledge (if not paid)
)}
{/* ── RIGHT: Editor ── */}
{/* Channel tabs */}
{CHANNELS.map(ch => { const m = CH_META[ch] const isActive = activeChannel === ch const isLive = activeChannels.includes(ch) const Icon = m.icon return ( ) })}
{/* A/B variant toggle */}
{hasVariantB ? ( <> {/* A/B stats */} {(currentTemplate?.sentCount || 0) + (variantB?.sentCount || 0) > 0 && (
A: {currentTemplate?.sentCount || 0} sent → {currentTemplate?.convertedCount || 0} paid ({currentTemplate?.sentCount ? Math.round(((currentTemplate?.convertedCount || 0) / currentTemplate.sentCount) * 100) : 0}%) B: {variantB.sentCount} sent → {variantB.convertedCount} paid ({variantB.sentCount ? Math.round((variantB.convertedCount / variantB.sentCount) * 100) : 0}%)
)} ) : ( )}
{/* Template name */}
{ setEditName(e.target.value); setDirty(true) }} className="text-lg font-black text-[#111827] bg-transparent border-b-2 border-transparent hover:border-gray-200 focus:border-[#1E40AF] outline-none w-full pb-1 transition-colors" placeholder="Template name..." />
{/* Email subject */} {activeChannel === "email" && (
{ setEditSubject(e.target.value); setDirty(true) }} className="w-full h-10 px-3 border-2 border-gray-200 text-sm font-medium placeholder:text-gray-300 focus:border-[#1E40AF] outline-none" placeholder="e.g. Your £{{amount}} pledge — payment details" />

Preview: {resolvePreview(editSubject || "")}

)} {/* Message body editor */}
{editBody.length} chars{activeChannel === "sms" ? ` · ${Math.ceil(editBody.length / 160)} SMS` : ""}