// ─── CRM Contacts Page ─────────────────────────────────────────────────────── function CRMPage() { const [contacts, setContacts] = React.useState([]); const [loading, setLoading] = React.useState(true); const [search, setSearch] = React.useState(''); const [statusFilter, setStatusFilter] = React.useState('all'); const [selected, setSelected] = React.useState(null); const { toasts, addToast } = C.useToast(); const handleCall = async (phoneNumber, contactName) => { addToast(`Dispatching call to ${phoneNumber}…`, 'info'); try { const res = await fetch('/api/call/single', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: phoneNumber, caller_name: contactName || '' }), }); const data = await res.json(); if (res.ok && data.status !== 'error') addToast(`Call dispatched to ${phoneNumber}`, 'success'); else addToast(data.message || data.detail || 'Call dispatch failed', 'error'); } catch (e) { addToast('Call dispatch error: ' + e.message, 'error'); } }; React.useEffect(() => { fetch('/api/contacts') .then(r => r.json()) .then(data => { setContacts(Array.isArray(data) ? data : []); setLoading(false); }) .catch(() => setLoading(false)); }, []); const filtered = contacts.filter(c => { const hot = c.is_booked; const warm = !c.is_booked && (c.total_calls || 0) > 1; const cold = !c.is_booked && (c.total_calls || 0) <= 1; const status = hot ? 'hot' : warm ? 'warm' : 'cold'; if (statusFilter !== 'all' && status !== statusFilter) return false; if (search) { const q = search.toLowerCase(); return (c.phone_number || '').includes(q) || (c.caller_name || '').toLowerCase().includes(q); } return true; }); const contact = contacts.find(c => c.phone_number === selected); const getStatus = c => c.is_booked ? 'hot' : (c.total_calls || 0) > 1 ? 'warm' : 'cold'; const STATUS_C = { hot: 'red', warm: 'amber', cold: 'blue' }; return (
+ Add Contact} /> {loading ? : (
setSearch(e.target.value)} placeholder="Search name or phone…" style={{ flex: 1, background: '#13161e', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#e8eaef', fontSize: 13, padding: '8px 12px', outline: 'none' }} />
{ const st = getStatus(c); return [
setSelected(c.phone_number)}>
{c.caller_name || 'Unknown'}
{c.phone_number}
, {c.total_calls ?? 0}, {st.charAt(0).toUpperCase() + st.slice(1)}, {c.last_seen ? new Date(c.last_seen).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }) : '—'},
setSelected(c.phone_number)}>View handleCall(c.phone_number, c.caller_name)}>Call
, ]; })} />
{contact ? ( setSelected(null)}>✕}>
{(() => { const st = getStatus(contact); return {st.charAt(0).toUpperCase() + st.slice(1)} Lead; })()} {[ { label: 'Phone', value: contact.phone_number }, { label: 'Name', value: contact.caller_name || '—' }, { label: 'Total Calls', value: contact.total_calls ?? 0 }, { label: 'Booking Status', value: contact.is_booked ? 'Booked' : 'Not booked' }, { label: 'Last Seen', value: contact.last_seen ? new Date(contact.last_seen).toLocaleString('en-IN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true }) : '—' }, { label: 'WhatsApp Unread', value: contact.whatsapp_unread_count ?? '—' }, ].map(r => (
{r.label}
{String(r.value)}
))}
handleCall(contact.phone_number, contact.caller_name)}>Trigger Call Send WhatsApp Book Appointment
) : (
Select a contact to view details
)}
)}
); } Object.assign(window, { CRMPage });