// ─── 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 (
http://127.0.0.1:3010)