/* global React, Icon, Avatar, StageChip, SourceChip, TemperatureDot, ChannelBadge, ChannelIcon, fmtAED */ const { useState, useEffect, useMemo } = React; function _initials(name) { if (!name) return '?'; const parts = String(name).replace(/[._@-]/g, ' ').split(/\s+/).filter(Boolean); if (!parts.length) return name.slice(0, 2).toUpperCase(); if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); } function _color(s) { const palette = ['#22c55e', '#3b82f6', '#a855f7', '#f97316', '#eab308', '#ec4899', '#06b6d4', '#84cc16']; let h = 0; for (let i = 0; i < (s || '').length; i++) h = ((h << 5) - h) + s.charCodeAt(i); return palette[Math.abs(h) % palette.length]; } function _relTime(iso) { if (!iso) return '—'; const d = new Date(iso); const diff = (Date.now() - d.getTime()) / 1000; if (diff < 60) return 'now'; if (diff < 3600) return `${Math.round(diff / 60)}m`; if (diff < 86400) return `${Math.round(diff / 3600)}h`; if (diff < 86400 * 7) return `${Math.round(diff / 86400)}d`; return d.toLocaleDateString(); } /* ============================================================ CONTACTS LIST (data table) ============================================================ */ function Contacts({ goto }) { const [search, setSearch] = useState(''); const [contacts, setContacts] = useState(null); const [error, setError] = useState(null); useEffect(() => { fetch('/api/contacts?limit=500', { credentials: 'include' }) .then((r) => r.ok ? r.json() : Promise.reject(r.statusText)) .then((d) => setContacts(d.items || [])) .catch((e) => setError(String(e))); }, []); const filtered = useMemo(() => { if (!contacts) return []; const q = search.toLowerCase(); if (!q) return contacts; return contacts.filter((c) => (c.name || '').toLowerCase().includes(q) || (c.company || '').toLowerCase().includes(q) || (c.email || '').toLowerCase().includes(q) || (c.linkedin_url || '').toLowerCase().includes(q) ); }, [contacts, search]); const total = contacts ? contacts.length : 0; return (
CRM

Contacts

{total} contacts
setSearch(e.target.value)} />
{filtered.length} of {total}
{contacts === null && !error && (
Loading…
)} {error && (
Failed to load contacts: {error}
)} {contacts && contacts.length === 0 && (
No contacts yet. They appear automatically when someone messages you on LinkedIn / WhatsApp / Email.
)} {contacts && contacts.length > 0 && ( {filtered.map((c) => { const channelKinds = [ c.linkedin_url ? 'linkedin' : null, c.whatsapp_number ? 'whatsapp' : null, c.instagram_handle ? 'instagram' : null, c.email ? 'email' : null, ].filter(Boolean); return ( goto('contact', c.id)} style={{ borderBottom: '1px solid var(--divider)', cursor: 'pointer' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> ); })}
Name Company Title Source Temp Stage Created Channels
{c.name || '(unnamed)'}
{c.company || '—'} {c.title || '—'} {c.source ? : '—'} {c.temperature ? : '—'} {c.pipeline_stage || 'New'} {_relTime(c.created_at)}
{channelKinds.length === 0 && } {channelKinds.map((k) => )}
)}
); } const thStyle = { padding: '10px 16px', textAlign: 'left', fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-dim)' }; const tdStyle = { padding: '10px 16px', verticalAlign: 'middle' }; /* ============================================================ ACCOUNTS LIST ============================================================ */ function Accounts({ goto }) { const [accounts, setAccounts] = useState(null); const [error, setError] = useState(null); useEffect(() => { fetch('/api/accounts?limit=500', { credentials: 'include' }) .then((r) => r.ok ? r.json() : Promise.reject(r.statusText)) .then((d) => setAccounts(d.items || [])) .catch((e) => setError(String(e))); }, []); return (
CRM

Accounts

{accounts ? accounts.length : '—'} companies
{accounts === null && !error && (
Loading…
)} {error && (
Failed to load accounts: {error}
)} {accounts && accounts.length === 0 && (
No accounts yet. Companies appear automatically as you add deals or tag contacts with a company.
)} {accounts && accounts.length > 0 && (
{accounts.map((co) => (
goto('account', co.id)}>
{co.name}
{co.industry || '—'} · {co.size || '—'}
{co.contact_count || 0} contacts
{co.deal_count || 0} deals
{co.deal_value_total > 0 &&
AED {fmtAED(co.deal_value_total)}
}
))}
)}
); } Object.assign(window, { Contacts, Accounts });