diff --git a/src/components/BulkEmailModal.tsx b/src/components/BulkEmailModal.tsx new file mode 100644 index 0000000..1de0b64 --- /dev/null +++ b/src/components/BulkEmailModal.tsx @@ -0,0 +1,380 @@ +import { useState, useEffect, useMemo } from 'react'; +import { api } from '@/lib/api'; +import type { Client, BulkEmailResult } from '@/types'; +import Modal from '@/components/Modal'; +import LoadingSpinner from '@/components/LoadingSpinner'; +import { Sparkles, Send, CheckCircle2, XCircle, Search, X, Users, ChevronLeft, ChevronRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface BulkEmailModalProps { + isOpen: boolean; + onClose: () => void; + clients: Client[]; + onComplete?: () => void; +} + +type Step = 'select' | 'configure' | 'preview' | 'done'; + +export default function BulkEmailModal({ isOpen, onClose, clients, onComplete }: BulkEmailModalProps) { + const [step, setStep] = useState('select'); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [search, setSearch] = useState(''); + const [stageFilter, setStageFilter] = useState(''); + const [purpose, setPurpose] = useState(''); + const [provider, setProvider] = useState<'anthropic' | 'openai'>('anthropic'); + const [generating, setGenerating] = useState(false); + const [result, setResult] = useState(null); + const [activePreview, setActivePreview] = useState(0); + const [sending, setSending] = useState(false); + const [sendResult, setSendResult] = useState<{ sent: number; failed: number } | null>(null); + + useEffect(() => { + if (isOpen) { + setStep('select'); + setSelectedIds(new Set()); + setSearch(''); + setStageFilter(''); + setPurpose(''); + setResult(null); + setActivePreview(0); + setSendResult(null); + } + }, [isOpen]); + + const filteredClients = useMemo(() => { + let filtered = clients; + if (search) { + const q = search.toLowerCase(); + filtered = filtered.filter(c => + `${c.firstName} ${c.lastName}`.toLowerCase().includes(q) || + c.email?.toLowerCase().includes(q) || + c.company?.toLowerCase().includes(q) + ); + } + if (stageFilter) { + filtered = filtered.filter(c => c.stage === stageFilter); + } + return filtered; + }, [clients, search, stageFilter]); + + const toggleClient = (id: string) => { + setSelectedIds(prev => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const selectAll = () => { + setSelectedIds(new Set(filteredClients.map(c => c.id))); + }; + + const clearAll = () => setSelectedIds(new Set()); + + const handleGenerate = async () => { + setGenerating(true); + try { + const res = await api.bulkGenerateEmails(Array.from(selectedIds), purpose, provider); + setResult(res); + setStep('preview'); + } catch (err) { + console.error('Bulk generate failed:', err); + } finally { + setGenerating(false); + } + }; + + const handleSendAll = async () => { + if (!result) return; + setSending(true); + try { + const res = await api.bulkSendEmails(result.batchId); + setSendResult({ sent: res.sent, failed: res.failed }); + setStep('done'); + onComplete?.(); + } catch (err) { + console.error('Bulk send failed:', err); + } finally { + setSending(false); + } + }; + + const handleSendSingle = async (emailId: string) => { + try { + await api.sendEmail(emailId); + // Update result to reflect sent + setResult(prev => prev ? { + ...prev, + results: prev.results.map(r => + r.email?.id === emailId ? { ...r, email: { ...r.email!, status: 'sent' as const } } : r + ), + } : null); + } catch (err) { + console.error('Send failed:', err); + } + }; + + const successResults = result?.results.filter(r => r.success && r.email) || []; + const stages = ['lead', 'prospect', 'onboarding', 'active', 'inactive']; + + return ( + +
+ {/* Steps indicator */} +
+ {(['select', 'configure', 'preview'] as Step[]).map((s, i) => ( +
+
i ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400' : 'bg-slate-100 dark:bg-slate-700 text-slate-400') + )}> + {i + 1} +
+ + {s === 'select' ? 'Select Clients' : s === 'configure' ? 'Configure' : 'Preview & Send'} + + {i < 2 &&
} +
+ ))} +
+ + {/* Step 1: Select Clients */} + {step === 'select' && ( +
+
+
+ + setSearch(e.target.value)} + placeholder="Search clients..." + className="w-full pl-9 pr-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ +
+ + {selectedIds.size} of {filteredClients.length} selected + +
+ + {selectedIds.size > 0 && } +
+
+ +
+ {filteredClients.map(c => ( + + ))} + {filteredClients.length === 0 && ( +

No clients found

+ )} +
+ +
+ +
+
+ )} + + {/* Step 2: Configure */} + {step === 'configure' && ( +
+
+ + {selectedIds.size} client{selectedIds.size !== 1 ? 's' : ''} selected +
+ +
+ +