// ─── WhatsApp Inbox Page ───────────────────────────────────────────────────── const WA_STATUS_DOT = { connected: '#22c55e', qr_pending: '#f59e0b', connecting: '#3b82f6', degraded: '#ef4444', disconnected: '#ef4444', disabled: '#6b7280', external: '#a78bfa', unknown: '#94a3b8', }; function WhatsAppPage() { const [waTab, setWaTab] = React.useState('Connector'); const [convos, setConvos] = React.useState([]); const [selected, setSelected] = React.useState(null); const [thread, setThread] = React.useState([]); const [msgInput, setMsgInput] = React.useState(''); const [templates, setTemplates] = React.useState([]); const [autoJobs, setAutoJobs] = React.useState([]); const [tplName, setTplName] = React.useState(''); const [loading, setLoading] = React.useState(true); const [sending, setSending] = React.useState(false); // ── Config state ── const [cfg, setCfg] = React.useState({ whatsapp_provider: 'baileys', whatsapp_service_url: 'http://127.0.0.1:3010', whatsapp_session_name: 'primary', whatsapp_webhook_secret: '', whatsapp_enabled: true, twilio_account_sid: '', twilio_auth_token: '', twilio_whatsapp_number: '' }); const setConfig = k => v => setCfg(p => ({ ...p, [k]: v })); // ── Connector state ── const [waStatus, setWaStatus] = React.useState(null); const [waLoading, setWaLoading] = React.useState(true); const [waActionBusy, setWaActionBusy] = React.useState(false); const [qrData, setQrData] = React.useState(null); const [qrOpen, setQrOpen] = React.useState(false); const [qrMsg, setQrMsg] = React.useState(''); const { toasts, addToast } = C.useToast(); // ── Fetch WA status ── const loadStatus = async () => { setWaLoading(true); try { const res = await fetch('/api/whatsapp/status'); const data = await res.json(); setWaStatus(data); } catch { setWaStatus({ connection_status: 'degraded', message: 'Could not reach WhatsApp service' }); } setWaLoading(false); }; React.useEffect(() => { loadStatus(); Promise.all([ fetch('/api/whatsapp/conversations').then(r => r.json()).catch(() => []), fetch('/api/whatsapp/templates').then(r => r.json()).catch(() => []), fetch('/api/automation/jobs').then(r => r.json()).catch(() => []), fetch('/api/config').then(r => r.json()).catch(() => ({})), ]).then(([c, t, j, configData]) => { const cv = Array.isArray(c) ? c : (Array.isArray(c?.items) ? c.items : []); const tp = Array.isArray(t) ? t : (Array.isArray(t?.items) ? t.items : []); const jb = Array.isArray(j) ? j : (Array.isArray(j?.items) ? j.items : []); setConvos(cv); setTemplates(tp); setAutoJobs(jb.filter(x => x.channel === 'whatsapp')); if (cv.length > 0 && !selected) { setSelected(cv[0].phone_number); } if (tp.length > 0) setTplName(tp[0].name); if (Object.keys(configData).length > 0) { setCfg(p => ({ ...p, ...configData })); } setLoading(false); }); }, []); React.useEffect(() => { if (!selected) return; fetch(`/api/whatsapp/conversations/${encodeURIComponent(selected)}`) .then(r => r.json()) .then(data => setThread(Array.isArray(data.messages) ? data.messages : [])) .catch(() => setThread([])); }, [selected]); const send = async () => { if (!msgInput.trim() || !selected || sending) return; setSending(true); try { await fetch('/api/whatsapp/messages/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone_number: selected, body_text: msgInput, template_name: null }), }); setThread(p => [...p, { role: 'agent', direction: 'outbound', body_text: msgInput, created_at: new Date().toISOString() }]); setMsgInput(''); fetch(`/api/whatsapp/conversations/${encodeURIComponent(selected)}/read`, { method: 'POST' }).catch(() => {}); } finally { setSending(false); } }; // ── Connector actions ── const handleConnect = async () => { setWaActionBusy(true); try { const res = await fetch('/api/whatsapp/connect', { method: 'POST' }); const data = await res.json(); if (res.ok) addToast('WhatsApp connect initiated', 'success'); else addToast(data.message || 'Connect failed', 'error'); await loadStatus(); } catch (e) { addToast('Connect error: ' + e.message, 'error'); } setWaActionBusy(false); }; const handleDisconnect = async () => { setWaActionBusy(true); try { const res = await fetch('/api/whatsapp/disconnect', { method: 'POST' }); const data = await res.json(); if (res.ok) addToast('WhatsApp disconnected', 'success'); else addToast(data.message || 'Disconnect failed', 'error'); await loadStatus(); } catch (e) { addToast('Disconnect error: ' + e.message, 'error'); } setWaActionBusy(false); }; const handleFetchQR = async () => { setQrMsg('Loading QR code…'); setQrData(null); setQrOpen(true); try { const res = await fetch('/api/whatsapp/qr'); const data = await res.json(); if (data.status === 'connected') { setQrMsg('WhatsApp is already connected!'); setQrData(null); addToast('Already connected', 'success'); } else if (data.qr_data_url) { setQrData(data.qr_data_url); setQrMsg('Scan this QR with WhatsApp → Linked Devices → Link a Device'); } else if (data.qr) { setQrData(data.qr); setQrMsg('Scan this QR with WhatsApp → Linked Devices → Link a Device'); } else { setQrMsg(data.message || 'QR code not available. Try clicking Connect first.'); } } catch (e) { setQrMsg('Failed to fetch QR: ' + e.message); } }; // Auto-poll status while QR modal is open React.useEffect(() => { if (!qrOpen) return; const iv = setInterval(async () => { try { const res = await fetch('/api/whatsapp/status'); const data = await res.json(); setWaStatus(data); if (data.connection_status === 'connected') { setQrOpen(false); addToast('WhatsApp connected successfully!', 'success'); } } catch {} }, 4000); return () => clearInterval(iv); }, [qrOpen]); const selectedConvo = convos.find(c => c.phone_number === selected); const selectedTpl = templates.find(t => t.name === tplName); const msgBody = m => m.body_text || m.body || m.message || m.text || m.content || ''; const msgRole = m => (m.direction === 'outbound' || m.role === 'agent') ? 'agent' : 'user'; const msgTime = m => { const t = m.created_at || m.timestamp; if (!t) return ''; return new Date(t).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true }); }; const connStatus = waStatus?.connection_status || 'unknown'; const dotColor = WA_STATUS_DOT[connStatus] || WA_STATUS_DOT.unknown; return (
{/* ─── CONNECTOR TAB ─── */} {waTab === 'Connector' && (
{/* Status overview */}
{connStatus.replace(/_/g, ' ')} } /> {waStatus?.service_url || '—'} } />
{/* Connection actions */} {waLoading ? 'Checking…' : 'Refresh Status'} }>
{connStatus === 'connected' && `✅ WhatsApp is linked and ready on session "${waStatus?.session_name || 'primary'}".`} {connStatus === 'qr_pending' && '⏳ QR is ready. Click "Show QR" and scan it from WhatsApp → Linked Devices.'} {connStatus === 'connecting' && '🔄 Trying to establish the WhatsApp Web session right now…'} {connStatus === 'disabled' && '⛔ WhatsApp automations are disabled in config.'} {connStatus === 'external' && '🔗 Using an external provider instead of the local Baileys sidecar.'} {connStatus === 'degraded' && `⚠️ ${waStatus?.message || 'WhatsApp service is not reachable.'}`} {connStatus === 'disconnected' && '❌ Not connected. Click Connect, then Show QR to scan.'} {connStatus === 'unknown' && 'Click "Refresh Status" to check the current state.'}
{waActionBusy ? 'Working…' : 'Connect'} 📱 Show QR Code Disconnect
{/* Quick-start guide */}
  1. Make sure the Baileys WhatsApp sidecar service is running (default: http://127.0.0.1:3010)
  2. Click Connect to initialize a session
  3. Click Show QR Code — a QR code will appear
  4. Open WhatsApp on your phone → Settings → Linked Devices → Link a Device
  5. Scan the QR code. The status will automatically update to Connected
  6. You're ready! The agent can now send WhatsApp messages and process follow-ups.
{/* WhatsApp Settings */}
Baileys Config
Twilio Config
{ try { addToast('Saving settings…', 'info'); await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg) }); addToast('Settings saved successfully', 'success'); loadStatus(); } catch(e) { addToast('Save failed: ' + e.message, 'error'); } }}>Save Settings
)} {/* ─── QR MODAL ─── */} {qrOpen && (
setQrOpen(false)}>
e.stopPropagation()}>
WhatsApp QR Login
{qrMsg}
setQrOpen(false)}>✕
{/* Status dot in modal */}
Status: {connStatus.replace(/_/g, ' ')}
{qrData ? (
WhatsApp QR
) : (
{connStatus === 'connected' ? '✅ Already connected!' : 'Waiting for QR code…'}
)}
Refresh QR setQrOpen(false)}>Close
)} {waTab === 'Messages' && (loading ? : (
{/* Conversation list */}
{convos.length === 0 ? (
No conversations yet
) : convos.map(c => (
setSelected(c.phone_number)} style={{ padding: '12px 14px', cursor: 'pointer', borderBottom: '1px solid rgba(255,255,255,0.04)', background: selected === c.phone_number ? 'rgba(90,126,245,0.1)' : 'transparent', transition: 'background 0.1s' }}>
{c.display_name || c.phone_number}
{(parseInt(c.unread_count) > 0) && {c.unread_count}} {c.last_message_at ? new Date(c.last_message_at).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true }) : ''}
{c.last_message_preview || '—'}
{c.phone_number}
))}
{/* Message thread */} {selectedConvo ? (
{selectedConvo.display_name || selectedConvo.phone_number}
{selectedConvo.phone_number}
Call View CRM
) : (
Select a conversation
)}
{thread.map((m, i) => { const role = msgRole(m); return (
{msgBody(m)}
{msgTime(m)}
); })}
setMsgInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} placeholder="Type a message…" style={{ flex: 1, background: '#1a1e28', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#e8eaef', fontSize: 13, padding: '8px 12px', outline: 'none', fontFamily: 'inherit' }} /> Send
{/* Template / context panel */}
{templates.length > 0 ? ( <> ({ value: t.name, label: t.label || t.name }))} /> {selectedTpl && (
{selectedTpl.body_text || selectedTpl.body || selectedTpl.template || '—'}
)} setMsgInput(selectedTpl?.body_text || selectedTpl?.body || selectedTpl?.template || '')}>Use Template ) : (
No templates configured.
)}
))} {waTab === 'Follow-up Jobs' && ( [ {(j.trigger || j.type || '').replace(/_/g, ' ')}, {j.phone_number}, {j.scheduled_for ? new Date(j.scheduled_for).toLocaleString('en-IN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true }) : '—'}, {j.status}, {j.label || j.template_name || '—'}, ])} /> )} {waTab === 'Templates' && (
{templates.length === 0 ? (
No templates configured.
) : templates.map(t => (
{t.label || t.name}
{t.name}
{t.body_text || t.body || t.template || '—'}
Edit Test Send
))} + New Template
)}
); } Object.assign(window, { WhatsAppPage });