// app/src/lib/crmApi.ts // CRM API client — canonical CRM routes // Implements the frontend adapter layer from Doc 10 (TypeScript Module Spec) import type { CrmContactListItem, CrmPerson, Client360Snapshot, CrmOpportunityCard, CrmTask, CrmLeadStageUpdate, KanbanColumn, ImportBatchSummary, ImportProposal, ImportReviewDecision, QdScoreEntry, OracleClientDataListItem, OracleClientDataDetail, OracleClientTimelineItem, } from '@/types/crmTypes'; import { buildVelocityHeaders } from '@/lib/velocitySession'; const API_BASE = import.meta.env.VITE_API_BASE_URL ?? ''; function getAuthHeaders(): Record { return Object.fromEntries(buildVelocityHeaders(undefined, false).entries()); } async function apiFetch(path: string, options?: RequestInit): Promise { const res = await fetch(`${API_BASE}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...getAuthHeaders(), ...(options?.headers ?? {}), }, }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(err.detail ?? `API error ${res.status}`); } return res.json() as Promise; } // ── Contact List ────────────────────────────────────────────────────────────── export async function fetchContacts(params: { search?: string; buyer_type?: string; status?: string; limit?: number; offset?: number; }): Promise<{ contacts: CrmContactListItem[]; total: number; limit: number; offset: number }> { const qs = new URLSearchParams(); if (params.search) qs.set('search', params.search); if (params.buyer_type) qs.set('buyer_type', params.buyer_type); if (params.status) qs.set('status', params.status); if (params.limit != null) qs.set('limit', String(params.limit)); if (params.offset != null) qs.set('offset', String(params.offset)); const res = await apiFetch<{ status: string; data: { contacts: CrmContactListItem[]; total: number; limit: number; offset: number } }>( `/api/crm/contacts?${qs}` ); return res.data; } export async function fetchContact(personId: string): Promise { const res = await apiFetch<{ status: string; data: CrmPerson }>(`/api/crm/contacts/${personId}`); return res.data; } // ── Client 360 ──────────────────────────────────────────────────────────────── export async function fetchClient360(personId: string): Promise { const res = await apiFetch<{ status: string; data: Client360Snapshot }>(`/api/crm/client-360/${personId}`); return res.data; } // ── Opportunities ───────────────────────────────────────────────────────────── export async function fetchOpportunities(params?: { stage?: string; limit?: number; offset?: number; }): Promise { const qs = new URLSearchParams(); if (params?.stage) qs.set('stage', params.stage); if (params?.limit != null) qs.set('limit', String(params.limit)); if (params?.offset != null) qs.set('offset', String(params.offset)); const res = await apiFetch<{ status: string; data: CrmOpportunityCard[] }>(`/api/crm/opportunities?${qs}`); return res.data; } export async function updateOpportunity(body: { opportunity_id: string; stage?: string; value?: number | null; probability?: number | null; expected_close_date?: string | null; next_action?: string | null; notes?: string | null; }): Promise { const { opportunity_id, ...payload } = body; const res = await apiFetch<{ status: string; data: CrmOpportunityCard }>(`/api/crm/opportunities/${opportunity_id}`, { method: 'PATCH', body: JSON.stringify(payload), }); return res.data; } // ── Tasks ───────────────────────────────────────────────────────────────────── export async function fetchTasks(params?: { status?: string; assigned_to?: string; limit?: number; }): Promise { const qs = new URLSearchParams(); if (params?.status) qs.set('status', params.status); if (params?.assigned_to) qs.set('assigned_to', params.assigned_to); if (params?.limit != null) qs.set('limit', String(params.limit)); const res = await apiFetch<{ status: string; data: CrmTask[] }>(`/api/crm/tasks?${qs}`); return res.data; } export async function createTask(body: { person_id: string; lead_id?: string; reminder_type?: string; title: string; notes?: string; due_at?: string; priority?: string; }): Promise<{ reminder_id: string; title: string }> { const res = await apiFetch<{ status: string; data: { reminder_id: string; title: string } }>('/api/crm/tasks', { method: 'POST', body: JSON.stringify(body), }); return res.data; } export async function updateTask(body: { reminder_id: string; status: 'pending' | 'done' | 'snoozed' | 'cancelled'; due_at?: string; notes?: string; }): Promise { const res = await apiFetch<{ status: string; data: CrmTask }>(`/api/crm/tasks/${body.reminder_id}`, { method: 'PATCH', body: JSON.stringify({ status: body.status, due_at: body.due_at, notes: body.notes, }), }); return res.data; } // ── Kanban ──────────────────────────────────────────────────────────────────── export async function fetchKanbanBoard(): Promise { const res = await apiFetch<{ status: string; data: KanbanColumn[] }>('/api/crm/kanban'); return res.data; } export async function updateLeadStage(body: { lead_id: string; status: string; notes?: string; }): Promise { const res = await apiFetch<{ status: string; data: CrmLeadStageUpdate }>(`/api/crm/leads/${body.lead_id}/stage`, { method: 'PATCH', body: JSON.stringify({ status: body.status, notes: body.notes, }), }); return res.data; } // ── QD Scores ───────────────────────────────────────────────────────────────── export async function fetchQdScore(personId: string): Promise<{ person_id: string; scores: Record; timeseries: Array<{ score_type: string; value: number; timestamp: string | null; signal_source: string | null; delta: number | null }>; }> { const res = await apiFetch<{ status: string; data: { person_id: string; scores: Record; timeseries: Array<{ score_type: string; value: number; timestamp: string | null; signal_source: string | null; delta: number | null }>; }; }>(`/api/crm/qd/${personId}`); return res.data; } // ── Import Batches ───────────────────────────────────────────────────────────── export async function uploadCrmImport(file: File, sourceSystem = 'csv_upload'): Promise<{ batch_id: string; row_count: number; mapped_columns: number; unmapped_columns: number; mapping_confidence: number; proposals_created: number; parse_errors: string[]; lifecycle: string; message: string; }> { const form = new FormData(); form.append('file', file); const res = await fetch(`${API_BASE}/api/crm/imports?source_system=${sourceSystem}`, { method: 'POST', headers: { ...getAuthHeaders() }, body: form, }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(err.detail ?? `Upload error ${res.status}`); } const json = await res.json(); return json.data; } export async function fetchImportBatches(lifecycle?: string): Promise { const qs = lifecycle ? `?lifecycle=${lifecycle}` : ''; const res = await apiFetch<{ status: string; data: ImportBatchSummary[] }>(`/api/crm/imports${qs}`); return res.data; } export async function fetchImportBatch(batchId: string): Promise<{ batch_id: string; filename: string; row_count: number; mapping_manifest: Record; lifecycle: string; proposals: ImportProposal[]; proposal_count: number; }> { const res = await apiFetch<{ status: string; data: ReturnType extends Promise ? R : never }>(`/api/crm/imports/${batchId}`); return res.data as Awaited>; } export async function reviewProposal( batchId: string, proposalId: string, decision: ImportReviewDecision, notes = '' ): Promise<{ decision_id: string; decision: string }> { const res = await apiFetch<{ status: string; data: { decision_id: string; decision: string } }>( `/api/crm/imports/${batchId}/review-proposal`, { method: 'PUT', body: JSON.stringify({ proposal_id: proposalId, decision, notes }), } ); return res.data; } export async function commitImportBatch(batchId: string): Promise<{ committed: number; skipped: number; errors: string[]; lifecycle: string; }> { const res = await apiFetch<{ status: string; data: { committed: number; skipped: number; errors: string[]; lifecycle: string }; }>(`/api/crm/imports/${batchId}/commit`, { method: 'POST' }); return res.data; } export async function fetchOracleClientData(params?: { search?: string; limit?: number; offset?: number; }): Promise<{ items: OracleClientDataListItem[]; count: number }> { const qs = new URLSearchParams(); if (params?.search) qs.set('search', params.search); if (params?.limit != null) qs.set('limit', String(params.limit)); if (params?.offset != null) qs.set('offset', String(params.offset)); const res = await apiFetch<{ status: string; data: OracleClientDataListItem[]; meta?: { count?: number } }>( `/api/crm/client-data?${qs}`, ); return { items: res.data, count: res.meta?.count ?? res.data.length }; } export async function fetchOracleClientDataDetail(personId: string): Promise { const res = await apiFetch<{ status: string; data: OracleClientDataDetail }>(`/api/crm/client-data/${personId}`); return res.data; } export async function patchOracleClientData( personId: string, patch: Record, ): Promise<{ person_id: string; updated: string[] }> { const res = await apiFetch<{ status: string; data: { person_id: string; updated: string[] } }>( `/api/crm/client-data/${personId}`, { method: 'PATCH', body: JSON.stringify(patch) }, ); return res.data; } export async function fetchOracleClientTimeline(personId: string): Promise { const res = await apiFetch<{ status: string; data: OracleClientTimelineItem[] }>( `/api/crm/client-data/${personId}/timeline`, ); return res.data; }