forked from sagnik/Velocity-OS
100 lines
2.9 KiB
TypeScript
100 lines
2.9 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { api } from '@/shared/lib/apiClient';
|
|
import { unwrapArray } from '@/shared/lib/apiShape';
|
|
|
|
/**
|
|
* useKanban — Pipeline Pillar kanban board data
|
|
* Returns leads grouped by stage.
|
|
*/
|
|
export function useKanban() {
|
|
const query = useQuery({
|
|
queryKey: ['kanban'],
|
|
queryFn: async () => {
|
|
const payload = await api.get<unknown>('/crm/pipeline/kanban?limit=250&offset=0');
|
|
return unwrapArray<RawKanbanStage>(payload, ['stages', 'kanban', 'columns', 'board']).map(normalizeStage);
|
|
},
|
|
staleTime: 0,
|
|
refetchInterval: 60_000,
|
|
refetchOnMount: 'always',
|
|
refetchOnWindowFocus: true,
|
|
retry: 1,
|
|
});
|
|
return {
|
|
stages: query.data ?? [],
|
|
isLoading: query.isLoading,
|
|
isError: query.isError,
|
|
error: query.error,
|
|
refetch: query.refetch,
|
|
};
|
|
}
|
|
|
|
export interface KanbanStage {
|
|
id: string;
|
|
label: string;
|
|
emoji: string;
|
|
leads: KanbanLead[];
|
|
}
|
|
|
|
export interface KanbanLead {
|
|
id: string;
|
|
name: string;
|
|
location?: string;
|
|
qdScore: number;
|
|
qdDelta?: number;
|
|
lastContactRelative: string;
|
|
lastContactChannel: string;
|
|
isVaultActive?: boolean;
|
|
}
|
|
|
|
interface RawKanbanStage {
|
|
id?: string;
|
|
stage?: string;
|
|
status?: string;
|
|
label?: string;
|
|
name?: string;
|
|
emoji?: string;
|
|
leads?: unknown;
|
|
items?: unknown;
|
|
data?: unknown;
|
|
records?: unknown;
|
|
rows?: unknown;
|
|
}
|
|
|
|
function normalizeStage(stage: RawKanbanStage): KanbanStage {
|
|
const id = String(stage.id ?? stage.stage ?? stage.status ?? 'new');
|
|
const leads = unwrapArray<RawKanbanLead>(stage, ['leads', 'items', 'data', 'records', 'rows']).map(normalizeLead);
|
|
return {
|
|
id,
|
|
label: String(stage.label ?? stage.name ?? id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())),
|
|
emoji: String(stage.emoji ?? id.slice(0, 1).toUpperCase()),
|
|
leads,
|
|
};
|
|
}
|
|
|
|
interface RawKanbanLead extends Partial<KanbanLead> {
|
|
person_id?: string;
|
|
lead_id?: string;
|
|
client_id?: string;
|
|
full_name?: string;
|
|
client_name?: string;
|
|
project_name?: string;
|
|
projects?: string;
|
|
qd_score?: number | string;
|
|
score?: number | string;
|
|
last_contact_relative?: string;
|
|
last_contact_channel?: string;
|
|
channel?: string;
|
|
}
|
|
|
|
function normalizeLead(lead: RawKanbanLead): KanbanLead {
|
|
return {
|
|
...lead,
|
|
id: String(lead.id ?? lead.person_id ?? lead.lead_id ?? lead.client_id ?? crypto.randomUUID()),
|
|
name: String(lead.name ?? lead.full_name ?? lead.client_name ?? 'Unnamed Client'),
|
|
location: lead.location ?? lead.project_name ?? lead.projects,
|
|
qdScore: Number.isFinite(Number(lead.qdScore ?? lead.qd_score ?? lead.score)) ? Number(lead.qdScore ?? lead.qd_score ?? lead.score) : 0,
|
|
lastContactRelative: String(lead.lastContactRelative ?? lead.last_contact_relative ?? 'No contact yet'),
|
|
lastContactChannel: String(lead.lastContactChannel ?? lead.last_contact_channel ?? lead.channel ?? 'crm'),
|
|
};
|
|
}
|