import { useEffect, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { MessageCircle, Search, Send, Phone, PhoneCall, Paperclip, MoreVertical, Link as LinkIcon, User, Settings, Bot, AlertCircle, CheckCheck, Clock, ChevronLeft, Inbox, Voicemail, Plus, Hash, } from 'lucide-react'; import { useStore } from '@/store/useStore'; import { fetchCommsThreads, fetchCommsMessages, sendCommsMessage, linkCommsThreadToPerson, fetchCommsSettings, } from '@/lib/commsApi'; import type { CommsThread, CommsMessage, CommsSettings, SendMessagePayload, } from '@/types/commsTypes'; /* ── Mock generators for demo / offline mode ─────────────────────────────── */ function generateMockThreads(): CommsThread[] { const now = new Date().toISOString(); return [ { threadId: 'mock-1', phoneE164: '+919876543210', displayName: 'Rahul Sharma', channel: 'whatsapp', status: 'open', unreadCount: 2, lastMessageAt: now, lastMessagePreview: 'Is the 3BHK still available?', provider: 'mock', createdAt: now, updatedAt: now, crmPerson: { id: 'p1', fullName: 'Rahul Sharma', primaryPhone: '+919876543210', leadStatus: 'hot', projectName: 'Atri Aqua', }, }, { threadId: 'mock-2', phoneE164: '+919988776655', displayName: 'Unknown Number', channel: 'whatsapp', status: 'open', unreadCount: 1, lastMessageAt: now, lastMessagePreview: 'Send me the brochure please', provider: 'mock', createdAt: now, updatedAt: now, }, { threadId: 'mock-3', phoneE164: '+911122334455', displayName: 'Priya Patel', channel: 'call', status: 'resolved', unreadCount: 0, lastMessageAt: now, provider: 'mock', createdAt: now, updatedAt: now, crmPerson: { id: 'p2', fullName: 'Priya Patel', primaryPhone: '+911122334455', leadStatus: 'qualified', projectName: 'Godrej Elevate', }, }, ]; } function generateMockMessages(threadId: string): CommsMessage[] { const now = new Date(); const t1 = new Date(now.getTime() - 1000 * 60 * 60 * 2).toISOString(); const t2 = new Date(now.getTime() - 1000 * 60 * 30).toISOString(); const t3 = new Date(now.getTime() - 1000 * 60 * 5).toISOString(); if (threadId === 'mock-1') { return [ { messageId: 'm1', threadId, direction: 'inbound', messageType: 'text', body: 'Hi, I saw your listing for Atri Aqua. Is the 3BHK still available?', deliveryStatus: 'read', createdAt: t1, provider: 'mock', senderName: 'Rahul Sharma', }, { messageId: 'm2', threadId, direction: 'outbound', messageType: 'text', body: 'Yes sir, absolutely. We have a premium corner unit on the 12th floor with marina view.', deliveryStatus: 'read', createdAt: t2, provider: 'mock', }, { messageId: 'm3', threadId, direction: 'inbound', messageType: 'text', body: 'What is the final price and can I schedule a visit this weekend?', deliveryStatus: 'delivered', createdAt: t3, provider: 'mock', senderName: 'Rahul Sharma', }, ]; } if (threadId === 'mock-2') { return [ { messageId: 'm4', threadId, direction: 'inbound', messageType: 'text', body: 'Send me the brochure please', deliveryStatus: 'delivered', createdAt: t3, provider: 'mock', senderName: 'Unknown', }, ]; } return []; } /* ── Component ───────────────────────────────────────────────────────────── */ export function Comms() { useStore(); const [threads, setThreads] = useState([]); const [activeThreadId, setActiveThreadId] = useState(null); const [messages, setMessages] = useState([]); const [settings, setSettings] = useState(null); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [composerText, setComposerText] = useState(''); const [sending, setSending] = useState(false); const [mockMode, setMockMode] = useState(false); const [showCrmRail, setShowCrmRail] = useState(true); const messagesEndRef = useRef(null); const activeThread = threads.find((t) => t.threadId === activeThreadId) || null; useEffect(() => { loadInitial(); }, []); useEffect(() => { if (activeThreadId) loadMessages(activeThreadId); }, [activeThreadId]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); async function loadInitial() { try { setLoading(true); const [threadsRes, settingsRes] = await Promise.all([ fetchCommsThreads({ limit: 50 }), fetchCommsSettings().catch(() => null), ]); setThreads(threadsRes.threads || []); setSettings(settingsRes); setMockMode(false); } catch (e) { console.warn('Comms backend unavailable, switching to mock mode', e); setMockMode(true); setThreads(generateMockThreads()); setSettings({ provider: 'mock', webhookSecretSet: false, autoLinkByPhone: false, createCrmInteractionOnInbound: false, defaultCountryCode: '91', }); } finally { setLoading(false); } } async function loadMessages(threadId: string) { try { if (mockMode) { setMessages(generateMockMessages(threadId)); return; } const res = await fetchCommsMessages(threadId, { limit: 100 }); setMessages(res.messages || []); } catch (e) { setMessages([]); } } async function handleSend(e?: React.FormEvent) { e?.preventDefault(); if (!composerText.trim() || !activeThreadId) return; const payload: SendMessagePayload = { messageType: 'text', body: composerText.trim(), }; const optimistic: CommsMessage = { messageId: `opt-${Date.now()}`, threadId: activeThreadId, direction: 'outbound', messageType: 'text', body: payload.body, deliveryStatus: 'pending', createdAt: new Date().toISOString(), provider: activeThread?.provider || 'mock', }; setMessages((prev) => [...prev, optimistic]); setComposerText(''); setSending(true); try { if (mockMode) { await new Promise((r) => setTimeout(r, 800)); setMessages((prev) => prev.map((m) => m.messageId === optimistic.messageId ? { ...m, deliveryStatus: 'sent', messageId: `mock-${Date.now()}` } : m ) ); } else { await sendCommsMessage(activeThreadId, payload); await loadMessages(activeThreadId); } } catch (err) { setMessages((prev) => prev.map((m) => m.messageId === optimistic.messageId ? { ...m, deliveryStatus: 'failed' } : m ) ); } finally { setSending(false); } } async function handleLinkPerson(personId: string) { if (!activeThreadId || mockMode) return; try { await linkCommsThreadToPerson(activeThreadId, { personId }); await loadInitial(); if (activeThreadId) await loadMessages(activeThreadId); } catch (e) { console.error(e); } } const filteredThreads = threads.filter((t) => { const q = searchQuery.toLowerCase(); return ( (t.displayName || '').toLowerCase().includes(q) || t.phoneE164.includes(q) || (t.lastMessagePreview || '').toLowerCase().includes(q) ); }); if (loading) { return (
Loading conversations…
); } /* ── Empty state: provider not configured ──────────────────────────────── */ if (!mockMode && (!settings || settings.provider === 'mock')) { return (

Conversations

Connect a WhatsApp provider to start receiving and sending messages. Until then, preview the interface with mock data.

); } return (
{/* ════════════════════════════════════════════════════════════════════ LEFT: Inbox ════════════════════════════════════════════════════════════════════ */} {/* ════════════════════════════════════════════════════════════════════ CENTER: Conversation Timeline ════════════════════════════════════════════════════════════════════ */}
{activeThread ? ( <> {/* ── Client Identity Strip ── */}
{activeThread.displayName?.[0]?.toUpperCase() || }

{activeThread.displayName || activeThread.phoneE164}

{activeThread.channel === 'whatsapp' && ( WhatsApp )} {activeThread.channel === 'call' && ( Call )}

{activeThread.phoneE164}

{/* ── Messages ── */}
{messages.map((msg) => (
{msg.messageType === 'text' &&

{msg.body}

} {msg.mediaUrl && (
media
)}
{new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {msg.direction === 'outbound' && ( <> {msg.deliveryStatus === 'pending' && } {msg.deliveryStatus === 'sent' && } {msg.deliveryStatus === 'delivered' && } {msg.deliveryStatus === 'read' && } {msg.deliveryStatus === 'failed' && } )}
))}
{/* ── Composer ── */}
{!activeThread.personId && (
This number is not linked to a CRM contact.
)}