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 (
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 (
Client Data
Velocity CRM Lens
Search, inspect, edit, and follow the client signal trail.
setQuery(event.target.value)}
placeholder="Search name, phone, email, project..."
className="h-10 border-white/10 bg-black/25 pl-9 text-sm text-zinc-100"
/>
{items.map((item) => (
setSelectedId(item.person_id)}
className={`mb-2 w-full rounded-2xl border p-3 text-left transition ${
item.person_id === selectedId
? 'border-cyan-300/40 bg-cyan-500/12 shadow-[0_0_24px_rgba(34,211,238,0.08)]'
: 'border-white/8 bg-white/[0.03] hover:bg-white/[0.06]'
}`}
>
{item.full_name.slice(0, 1)}
{item.full_name}
{item.projects || item.buyer_type || 'No project interest'}
QD {Math.round((item.qd_score || 0) * 100)}
{item.days_since_contact == null ? 'No contact clock' : `${item.days_since_contact}d since contact`}
))}
{!items.length && (
{loadingList ? 'Loading clients...' : 'No clients found.'}
)}
{!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...
}
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.
}
Next Best Action
{fmt(selected.next_best_action ?? profile.recommended_action)}
{fmt(profile.rationale)}
Engagement Intelligence
Extracted Facts
{(detail?.extracted_facts ?? []).slice(0, 10).map((fact, index) => (
{fmt(fact.fact_type)}
{fmt(fact.fact_value)}
))}
{!(detail?.extracted_facts ?? []).length &&
No extracted facts loaded.
}
Open Opportunities
{(detail?.opportunities ?? []).slice(0, 8).map((opp, index) => (
{fmt(opp.project_name ?? opp.stage)}
))}
{!(detail?.opportunities ?? []).length &&
No opportunities loaded.
}
)}
);
}