// Client CRM screen — table + selectable profile panel. (() => { const { Card, Badge, Avatar, Tabs, Input } = window.TechyFuelOSDesignSystem_be0222; const TF_STATUS = { active: { tone: 'success', label: 'Active client' }, inactive: { tone: 'neutral', label: 'Inactive' }, lead: { tone: 'brand', label: 'Lead' }, }; // Capitalize the first letter of each word as the user types. function titleCase(s) { return String(s).replace(/(^|\s)([a-z])/g, (m, p, c) => p + c.toUpperCase()); } const CLIENT_CURRENCIES = ['PKR', 'USD', 'EUR', 'GBP', 'AED', 'INR']; const CURRENCY_SYMBOL = { PKR: 'Rs', USD: '$', EUR: '€', GBP: '£', AED: 'AED', INR: '₹' }; function fmtValue(n, currency) { const sym = CURRENCY_SYMBOL[currency || 'USD'] || ''; if (!n) return sym + '0/mo'; return sym + Number(n).toLocaleString() + '/mo'; } function Th({ children, w }) { return {children}; } function ClientRow({ c, selected, onClick }) { const [hover, setHover] = React.useState(false); const s = TF_STATUS[c.status] || TF_STATUS.lead; const displayName = c.company || c.name; return ( setHover(true)} onMouseLeave={() => setHover(false)} style={{ cursor: 'pointer', background: selected ? 'var(--blue-50)' : hover ? 'var(--slate-50)' : 'transparent', transition: 'background var(--dur-fast) var(--ease-out)' }}>
{displayName}
{c.website || c.email}
{c.name} {s.label} {c.industry || '—'} {c.email} {fmtValue(c.monthly_value, c.currency)} ); } function ProfileField({ icon, label, value }) { return (
{label} {value}
); } function CRM() { useLucide(); const [clients, setClients] = React.useState([]); const [selId, setSelId] = React.useState(null); const [search, setSearch] = React.useState(''); const [statusFilter, setStatusFilter] = React.useState('all'); const [loading, setLoading] = React.useState(true); const [modalOpen, setModalOpen] = React.useState(false); const [saving, setSaving] = React.useState(false); const [deleting, setDeleting] = React.useState(false); const [confirmDelete, setConfirmDelete] = React.useState(false); const [form, setForm] = React.useState({ name: '', company: '', email: '', website: '', whatsapp: '', phone: '', industry: '', monthly_value: '', currency: 'PKR', status: 'active' }); const [addError, setAddError] = React.useState(''); const [editOpen, setEditOpen] = React.useState(false); const [editForm, setEditForm] = React.useState({}); const [editSaving, setEditSaving] = React.useState(false); // Client detail — related projects, invoices, notes/feedback. const [detailTab, setDetailTab] = React.useState('projects'); const [cProjects, setCProjects] = React.useState([]); const [cInvoices, setCInvoices] = React.useState([]); const [cNotes, setCNotes] = React.useState([]); const [cMeetings, setCMeetings] = React.useState([]); const [mtg, setMtg] = React.useState({ title: '', scheduled_at: '', location: '' }); const [noteText, setNoteText] = React.useState(''); React.useEffect(() => { if (!selId || !window.API) { setCProjects([]); setCInvoices([]); setCNotes([]); return; } (async () => { try { const r = await window.API.getProjects(); if (r.data) setCProjects(r.data.filter(p => p.client_id === selId)); } catch {} try { const r = await window.API.getInvoices(); if (r.data) setCInvoices(r.data.filter(iv => iv.client_id === selId)); } catch {} try { const r = await window.API.getClientNotes(selId); if (r.data) setCNotes(r.data); } catch {} try { const r = await window.API.getMeetings(selId); if (r.data) setCMeetings(r.data); } catch {} })(); }, [selId]); async function addNote() { if (!noteText.trim() || !selId || !window.API) return; try { const { data } = await window.API.createClientNote({ client_id: selId, body: noteText.trim() }); if (data) setCNotes(prev => [data, ...prev]); setNoteText(''); } catch { alert('Could not add note.'); } } async function bookMeeting() { if (!mtg.title.trim() || !mtg.scheduled_at || !selId || !window.API) return; try { const { data } = await window.API.createMeeting({ client_id: selId, title: mtg.title.trim(), scheduled_at: mtg.scheduled_at, location: mtg.location }); if (data) setCMeetings(prev => [data, ...prev]); setMtg({ title: '', scheduled_at: '', location: '' }); } catch { alert('Could not book meeting.'); } } async function delMeeting(id) { setCMeetings(prev => prev.filter(m => m.id !== id)); try { await window.API.deleteMeeting(id); } catch {} } async function delNote(id) { setCNotes(prev => prev.filter(n => n.id !== id)); try { await window.API.deleteClientNote(id); } catch {} } const [editError, setEditError] = React.useState(''); function set(k, v) { setForm(f => ({ ...f, [k]: v })); } function setEF(k, v) { setEditForm(f => ({ ...f, [k]: v })); } function openEdit(c) { setEditError(''); setEditForm({ name: c.name || '', company: c.company || '', email: c.email || '', website: c.website || '', whatsapp: c.whatsapp || '', phone: c.phone || '', industry: c.industry || '', monthly_value: c.monthly_value ? String(c.monthly_value) : '', currency: c.currency || 'PKR', status: c.status || 'active', }); setEditOpen(true); } async function handleUpdateClient() { if (!sel || !editForm.name?.trim()) return; setEditSaving(true); setEditError(''); try { const payload = { name: editForm.name.trim(), status: editForm.status }; payload.company = editForm.company || null; payload.email = editForm.email || null; payload.website = editForm.website || null; payload.whatsapp = editForm.whatsapp || null; payload.phone = editForm.phone || null; payload.industry = editForm.industry || null; payload.monthly_value = editForm.monthly_value ? Number(editForm.monthly_value) : null; payload.currency = editForm.currency || 'PKR'; if (!window.API) { setEditOpen(false); return; } const { data, error } = await window.API.updateClient(sel.id, payload); if (error) { setEditError(error.message || 'Could not save changes. Please try again.'); return; } setClients(prev => prev.map(c => c.id === sel.id ? { ...c, ...(data || payload) } : c)); setEditOpen(false); } finally { setEditSaving(false); } } React.useEffect(() => { if (!window.API) { setLoading(false); return; } (async () => { try { const { data } = await window.API.getClients(); if (Array.isArray(data) && data.length > 0) { setClients(data); setSelId(data[0].id); } } catch {} setLoading(false); })(); }, []); async function handleDeleteClient(id) { setDeleting(true); if (window.API) { try { await window.API.deleteClient(id); } catch(_) {} } const remaining = clients.filter(c => c.id !== id); setClients(remaining); setSelId(remaining[0]?.id || null); setConfirmDelete(false); setDeleting(false); } async function handleAddClient() { if (!form.name.trim()) return; setSaving(true); setAddError(''); try { const payload = { name: form.name, status: form.status }; if (form.company) payload.company = form.company; if (form.email) payload.email = form.email; if (form.website) payload.website = form.website; if (form.whatsapp) payload.whatsapp = form.whatsapp; if (form.phone) payload.phone = form.phone; if (form.industry) payload.industry = form.industry; if (form.monthly_value) payload.monthly_value = Number(form.monthly_value); payload.currency = form.currency || 'PKR'; if (window.API) { const { data, error } = await window.API.createClient(payload); if (error) { setAddError(error.message || 'Could not add the client. Please try again.'); return; } if (data) { setClients(prev => [...prev, data]); setSelId(data.id); } } setModalOpen(false); setForm({ name: '', company: '', email: '', website: '', whatsapp: '', phone: '', industry: '', monthly_value: '', currency: 'PKR', status: 'active' }); } finally { setSaving(false); } } const filtered = clients.filter(c => { const q = search.toLowerCase(); const matchesSearch = !q || (c.company || c.name || '').toLowerCase().includes(q) || (c.name || '').toLowerCase().includes(q); const matchesStatus = statusFilter === 'all' || (c.status || 'lead') === statusFilter; return matchesSearch && matchesStatus; }); if (loading) { return
Loading…
; } const sel = clients.find(c => c.id === selId) || clients[0] || null; const s = sel ? (TF_STATUS[sel.status] || TF_STATUS.lead) : TF_STATUS.lead; const displayName = sel ? (sel.company || sel.name) : ''; const activeCount = clients.filter(c => c.status === 'active').length; return (

Client CRM

{clients.length} {clients.length === 1 ? 'client' : 'clients'}{activeCount ? ` · ${activeCount} active` : ''}

} value={search} onChange={e => setSearch(e.target.value)} />
{filtered.length} of {clients.length}
{filtered.map(c => { setSelId(c.id); setConfirmDelete(false); }} />)}
CompanyContactStatusIndustryEmailValue
{!sel && (
No clients yet
Add your first client to see their profile here.
)} {sel && ( <>
{displayName}
{s.label}
{confirmDelete ?
:
}
{[['mail', 'Email'], ['message-circle', 'WhatsApp'], ['calendar-plus', 'Meeting']].map(([ic, l]) => ( ))}
{/* Public portal link — creates (or reuses) a shareable token so the client can open their portal with NO login. */}
{/* Related tabs */}
{[['projects',`Projects (${cProjects.length})`],['invoices',`Invoices (${cInvoices.length})`],['notes',`Feedback (${cNotes.length})`],['meetings',`Meetings (${cMeetings.length})`]].map(([id,label]) => ( ))}
{detailTab === 'projects' && (
{cProjects.length === 0 &&
No projects for this client.
} {cProjects.map(p => (
window.TFNavigate && window.TFNavigate('projects')} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', cursor: 'pointer' }}> {p.name} {p.progress || 0}%
))}
)} {detailTab === 'invoices' && (
{cInvoices.length === 0 &&
No invoices for this client.
} {cInvoices.map(iv => (
window.TFNavigate && window.TFNavigate('finance')} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', cursor: 'pointer' }}> {iv.number || iv.invoice_number || 'Invoice'} {fmtValue(iv.amount || iv.total)} {iv.status || 'unpaid'}
))}
)} {detailTab === 'notes' && (
setNoteText(e.target.value)} onKeyDown={e => e.key==='Enter'&&addNote()} placeholder="Add note / feedback…" style={{ flex: 1, height: 34, padding: '0 10px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', fontSize: 'var(--text-xs)', fontFamily: 'var(--font-sans)', outline: 'none' }} />
{cNotes.length === 0 &&
No notes yet.
} {cNotes.map(n => (
{n.body || n.note || n.content}
{n.created_at ? new Date(String(n.created_at).replace(' ','T')+'Z').toLocaleDateString('en-GB') : ''}
))}
)} {detailTab === 'meetings' && (
setMtg(m => ({ ...m, title: e.target.value }))} placeholder="Meeting title (e.g. Monthly review)" style={{ height: 32, padding: '0 10px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', fontSize: 'var(--text-xs)', fontFamily: 'var(--font-sans)', outline: 'none' }} /> setMtg(m => ({ ...m, scheduled_at: e.target.value }))} style={{ height: 32, padding: '0 10px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', fontSize: 'var(--text-xs)', fontFamily: 'var(--font-sans)', outline: 'none' }} /> setMtg(m => ({ ...m, location: e.target.value }))} placeholder="Location or video link (optional)" style={{ height: 32, padding: '0 10px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', fontSize: 'var(--text-xs)', fontFamily: 'var(--font-sans)', outline: 'none' }} />
{cMeetings.length === 0 &&
No meetings yet.
} {cMeetings.map(m => (
{m.title}
{m.scheduled_at ? new Date(String(m.scheduled_at).replace(' ','T')+'Z').toLocaleString('en-GB',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : ''}{m.location ? ' · ' + m.location : ''}
))}
)}
)}
{ setModalOpen(false); setAddError(''); }} title="Add client" onSubmit={handleAddClient} loading={saving} submitLabel="Add client"> {addError && (
{addError}
)}
set('name', titleCase(e.target.value))} /> set('company', e.target.value)} />
set('email', e.target.value)} /> set('website', e.target.value)} />
set('whatsapp', e.target.value)} /> set('phone', e.target.value)} />
set('industry', e.target.value)} />
set('monthly_value', e.target.value)} />
setEditOpen(false)} title="Edit client" onSubmit={handleUpdateClient} loading={editSaving} submitLabel="Save changes"> {editError && (
{editError}
)}
setEF('name', titleCase(e.target.value))} /> setEF('company', e.target.value)} />
setEF('email', e.target.value)} /> setEF('website', e.target.value)} />
setEF('whatsapp', e.target.value)} /> setEF('phone', e.target.value)} />
setEF('industry', e.target.value)} />
setEF('monthly_value', e.target.value)} />
); } Object.assign(window, { CRM }); })();