forked from sagnik/Project_Velocity
262 lines
9.8 KiB
TypeScript
262 lines
9.8 KiB
TypeScript
// 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,
|
|
KanbanColumn,
|
|
ImportBatchSummary,
|
|
ImportProposal,
|
|
ImportReviewDecision,
|
|
QdScoreEntry,
|
|
OracleClientDataListItem,
|
|
OracleClientDataDetail,
|
|
OracleClientTimelineItem,
|
|
} from '@/types/crmTypes';
|
|
import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient';
|
|
|
|
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? '';
|
|
|
|
function getAuthHeaders(): Record<string, string> {
|
|
const token = localStorage.getItem(VELOCITY_TOKEN_KEY);
|
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
}
|
|
|
|
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
|
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<T>;
|
|
}
|
|
|
|
// ── 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<CrmPerson> {
|
|
const res = await apiFetch<{ status: string; data: CrmPerson }>(`/api/crm/contacts/${personId}`);
|
|
return res.data;
|
|
}
|
|
|
|
// ── Client 360 ────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchClient360(personId: string): Promise<Client360Snapshot> {
|
|
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<CrmOpportunityCard[]> {
|
|
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;
|
|
}
|
|
|
|
// ── Tasks ─────────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchTasks(params?: {
|
|
status?: string;
|
|
assigned_to?: string;
|
|
limit?: number;
|
|
}): Promise<CrmTask[]> {
|
|
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;
|
|
}
|
|
|
|
// ── Kanban ────────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchKanbanBoard(): Promise<KanbanColumn[]> {
|
|
const res = await apiFetch<{ status: string; data: KanbanColumn[] }>('/api/crm/kanban');
|
|
return res.data;
|
|
}
|
|
|
|
// ── QD Scores ─────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchQdScore(personId: string): Promise<{
|
|
person_id: string;
|
|
scores: Record<string, QdScoreEntry>;
|
|
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<string, QdScoreEntry>;
|
|
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<ImportBatchSummary[]> {
|
|
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<string, unknown>;
|
|
lifecycle: string;
|
|
proposals: ImportProposal[];
|
|
proposal_count: number;
|
|
}> {
|
|
const res = await apiFetch<{ status: string; data: ReturnType<typeof fetchImportBatch> extends Promise<infer R> ? R : never }>(`/api/crm/imports/${batchId}`);
|
|
return res.data as Awaited<ReturnType<typeof fetchImportBatch>>;
|
|
}
|
|
|
|
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<OracleClientDataDetail> {
|
|
const res = await apiFetch<{ status: string; data: OracleClientDataDetail }>(`/api/crm/client-data/${personId}`);
|
|
return res.data;
|
|
}
|
|
|
|
export async function patchOracleClientData(
|
|
personId: string,
|
|
patch: Record<string, string | null>,
|
|
): 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<OracleClientTimelineItem[]> {
|
|
const res = await apiFetch<{ status: string; data: OracleClientTimelineItem[] }>(
|
|
`/api/crm/client-data/${personId}/timeline`,
|
|
);
|
|
return res.data;
|
|
}
|