Initial commit: Velocity-OS migration
This commit is contained in:
133
webos/src/shared/hooks/useClient360.ts
Normal file
133
webos/src/shared/hooks/useClient360.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/shared/lib/apiClient';
|
||||
|
||||
/**
|
||||
* useClient360 — fetch unified client entity
|
||||
* Feeds: CRM lead data + QD score + pipeline stage + contact info
|
||||
*/
|
||||
export function useClient360(personId: string) {
|
||||
const query = useQuery({
|
||||
queryKey: ['client360', personId],
|
||||
queryFn: () => api.get<Client360Data>(`/crm/leads/${personId}/360`),
|
||||
staleTime: 30_000,
|
||||
enabled: !!personId,
|
||||
});
|
||||
return { client: query.data, isLoading: query.isLoading, error: query.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* useConversations — unified comms feed for a lead
|
||||
*/
|
||||
export function useConversations(personId: string) {
|
||||
const qc = useQueryClient();
|
||||
const query = useQuery({
|
||||
queryKey: ['conversations', personId],
|
||||
queryFn: () => api.get<ConversationEvent[]>(`/comms/threads/${personId}`),
|
||||
staleTime: 10_000,
|
||||
enabled: !!personId,
|
||||
});
|
||||
|
||||
const sendWhatsApp = (text: string) =>
|
||||
api.post(`/comms/send`, { person_id: personId, channel: 'whatsapp', text })
|
||||
.then(() => qc.invalidateQueries({ queryKey: ['conversations', personId] }));
|
||||
|
||||
return { events: query.data ?? [], isLoading: query.isLoading, sendWhatsApp };
|
||||
}
|
||||
|
||||
/**
|
||||
* useClientProperties — linked property interests
|
||||
*/
|
||||
export function useClientProperties(personId: string) {
|
||||
const query = useQuery({
|
||||
queryKey: ['client-properties', personId],
|
||||
queryFn: () => api.get<PropertyInterest[]>(`/crm/leads/${personId}/properties`),
|
||||
staleTime: 60_000,
|
||||
enabled: !!personId,
|
||||
});
|
||||
return { properties: query.data ?? [], isLoading: query.isLoading };
|
||||
}
|
||||
|
||||
/**
|
||||
* useClientTasks — tasks for a specific lead
|
||||
*/
|
||||
export function useClientTasks(personId: string) {
|
||||
const qc = useQueryClient();
|
||||
const query = useQuery({
|
||||
queryKey: ['client-tasks', personId],
|
||||
queryFn: () => api.get<Task[]>(`/crm/leads/${personId}/tasks`),
|
||||
staleTime: 30_000,
|
||||
enabled: !!personId,
|
||||
});
|
||||
|
||||
const markDone = (taskId: string) =>
|
||||
api.patch(`/crm/tasks/${taskId}`, { status: 'done' })
|
||||
.then(() => qc.invalidateQueries({ queryKey: ['client-tasks', personId] }));
|
||||
|
||||
const snooze = (taskId: string) =>
|
||||
api.patch(`/crm/tasks/${taskId}`, { status: 'snoozed' })
|
||||
.then(() => qc.invalidateQueries({ queryKey: ['client-tasks', personId] }));
|
||||
|
||||
// Group tasks
|
||||
const all = query.data ?? [];
|
||||
const tasks = all.map(t => ({
|
||||
...t,
|
||||
group: t.status === 'done' ? 'completed'
|
||||
: t.isDueToday ? 'today'
|
||||
: 'upcoming',
|
||||
})) as any[];
|
||||
|
||||
return { tasks, isLoading: query.isLoading, markDone, snooze };
|
||||
}
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────
|
||||
export interface Client360Data {
|
||||
id: string;
|
||||
name: string;
|
||||
location?: string;
|
||||
primaryPhone?: string;
|
||||
avatarUrl?: string;
|
||||
qdScore: number;
|
||||
qdDelta: number;
|
||||
stageName: string;
|
||||
stageEmoji: string;
|
||||
lastContactRelative: string;
|
||||
lastContactChannel: string;
|
||||
aiInsight?: string;
|
||||
extractedFacts?: Record<string, string>;
|
||||
objections?: string[];
|
||||
qdHistory?: { date: string; score: number; label?: string }[];
|
||||
}
|
||||
|
||||
interface ConversationEvent {
|
||||
id: string;
|
||||
type: 'whatsapp' | 'call' | 'email';
|
||||
timestamp: string;
|
||||
timestampRelative: string;
|
||||
messages?: { sender: 'client' | 'you'; text: string; status?: '✓' | '✓✓' }[];
|
||||
duration?: string;
|
||||
direction?: 'inbound' | 'outbound';
|
||||
keyMoments?: string[];
|
||||
hasTranscript?: boolean;
|
||||
subject?: string;
|
||||
}
|
||||
|
||||
interface PropertyInterest {
|
||||
id: string;
|
||||
projectName: string;
|
||||
unitName: string;
|
||||
config: string;
|
||||
area: string;
|
||||
price: string;
|
||||
thumbnailUrl?: string;
|
||||
isPrimary: boolean;
|
||||
engagementLevel: 'High' | 'Medium' | 'Low';
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
label: string;
|
||||
dueAt?: string;
|
||||
status: 'pending' | 'done' | 'snoozed';
|
||||
isDueToday?: boolean;
|
||||
isAIGenerated?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user