import { useEffect, useMemo, useState } from 'react'; import { Activity, AlertTriangle, ArrowRight, Check, Clock, Database, Mail, Phone, Search, Sparkles, UserRound, Zap, } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { fetchOracleClientData, fetchOracleClientDataDetail, patchOracleClientData, } from '@/lib/crmApi'; import type { OracleClientDataDetail, OracleClientDataListItem } from '@/types/crmTypes'; const STAGES = [ 'new', 'contacted', 'qualified', 'site_visit_scheduled', 'site_visited', 'negotiation', 'booking_initiated', 'booked', ]; function fmt(value: unknown): string { if (value == null || value === '') return '-'; if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(2); return String(value); } function shortDate(value: unknown): string { if (!value) return '-'; const date = new Date(String(value)); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString(); } function FieldRow({ label, value }: { label: string; value: unknown }) { return (
{label} {fmt(value)}
); } function EmptyDiagnostic({ error, loading }: { error: string | null; loading: boolean }) { return (
{loading ? : }

Client data unavailable

{loading ? 'Loading Velocity CRM data...' : 'The CRM lens has no rows to show.'}

{error ? error : 'The local frontend is alive, but the CRM API returned no clients. Start the local backend, start Docker/Postgres, apply the canonical schema, and seed synthetic_crm_v2 before verifying this tab.'}

Expected local stack

Backend: http://127.0.0.1:8001

DB: 127.0.0.1:54329 / velocity_local

Dataset: db assets/synthetic_crm_v2/csv

); } export function ClientDataLens() { const [query, setQuery] = useState(''); const [items, setItems] = useState([]); const [selectedId, setSelectedId] = useState(null); const [detail, setDetail] = useState(null); const [loadingList, setLoadingList] = useState(false); const [loadingDetail, setLoadingDetail] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [draft, setDraft] = useState({ full_name: '', primary_email: '', primary_phone: '', buyer_type: '', communication_preference: '', best_contact_time: '', budget_band: '', urgency: '', }); useEffect(() => { const handle = window.setTimeout(() => { setLoadingList(true); setError(null); void fetchOracleClientData({ search: query, limit: 80 }) .then(({ items: rows }) => { setItems(rows); setSelectedId((current) => current ?? rows[0]?.person_id ?? null); if (!rows.length) { setDetail(null); } }) .catch((err) => { setItems([]); setSelectedId(null); setDetail(null); setError(err instanceof Error ? err.message : 'Failed to reach the CRM client data API.'); }) .finally(() => setLoadingList(false)); }, 180); return () => window.clearTimeout(handle); }, [query]); useEffect(() => { if (!selectedId) return; setLoadingDetail(true); setError(null); void fetchOracleClientDataDetail(selectedId) .then((data) => { const profile = data.profile ?? {}; setDetail(data); setDraft({ full_name: String(profile.full_name ?? ''), primary_email: String(profile.primary_email ?? ''), primary_phone: String(profile.primary_phone ?? ''), buyer_type: String(profile.buyer_type ?? ''), communication_preference: String(profile.communication_preference ?? ''), best_contact_time: String(profile.best_contact_time ?? ''), budget_band: String(profile.budget_band ?? ''), urgency: String(profile.urgency ?? ''), }); }) .catch((err) => { setDetail(null); setError(err instanceof Error ? err.message : 'Failed to load the selected client record.'); }) .finally(() => setLoadingDetail(false)); }, [selectedId]); const selected = useMemo( () => items.find((item) => item.person_id === selectedId) ?? items[0] ?? null, [items, selectedId], ); const profile = detail?.profile ?? {}; const currentStage = String(profile.lead_status ?? selected?.lead_status ?? 'new'); const qdScore = Number(selected?.qd_score ?? 0); const activeStageIndex = Math.max(0, STAGES.indexOf(currentStage)); async function saveDraft() { if (!selectedId) return; setSaving(true); setError(null); try { await patchOracleClientData(selectedId, draft); const fresh = await fetchOracleClientDataDetail(selectedId); setDetail(fresh); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save client data.'); } finally { setSaving(false); } } return (
{!selected ? ( ) : (
{error && (
{error}
)}

Contact Record

{selected.full_name}

{fmt(selected.primary_phone)} {fmt(selected.primary_email)} {fmt(selected.broker_name)}

QD Signal

{Math.round(qdScore * 100)}

Last Contact

{shortDate(selected.last_contact_at)}

Next Action

{fmt(selected.next_best_action)}

{STAGES.map((stage, index) => { const active = activeStageIndex >= index; return (
{stage.replace(/_/g, ' ')}
); })}
{loadingDetail &&

Loading record...

}

Editable Details

Typed API writes only. Oracle SQL remains read-only.

{Object.entries(draft).map(([key, value]) => ( ))}

Property Interests

{(detail?.property_interests ?? []).slice(0, 8).map((interest, index) => (

{fmt(interest.project_name)}

{fmt(interest.configuration)} | {fmt(interest.budget_min)}-{fmt(interest.budget_max)}

Priority {fmt(interest.priority)}

))} {!(detail?.property_interests ?? []).length &&

No property interests loaded for this client.

}

Unified Engagement Timeline

{(detail?.timeline ?? []).slice(0, 24).map((event) => (

{fmt(event.title || event.type)}

{fmt(event.summary)}

{shortDate(event.date)}
))} {!(detail?.timeline ?? []).length &&

No timeline events loaded. Seed v2 interactions/messages/calls/visits to populate this rail.

}
)}
); }