fix: restore Velocity OS entity and pillar views
Some checks failed
Velocity-OS Deployment Pipeline / lint (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / notify-ingress (push) Has been cancelled
Some checks failed
Velocity-OS Deployment Pipeline / lint (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / notify-ingress (push) Has been cancelled
This commit is contained in:
@@ -8,7 +8,10 @@ import { api } from '@/shared/lib/apiClient';
|
||||
export function useClient360(personId: string) {
|
||||
const query = useQuery({
|
||||
queryKey: ['client360', personId],
|
||||
queryFn: () => api.get<Client360Data>(`/crm/leads/${personId}/360`),
|
||||
queryFn: async () => {
|
||||
const payload = await api.get<Wrapped<Client360Snapshot>>(`/crm/client-360/${personId}`);
|
||||
return mapClient360(payload.data);
|
||||
},
|
||||
staleTime: 30_000,
|
||||
enabled: !!personId,
|
||||
});
|
||||
@@ -22,7 +25,10 @@ export function useConversations(personId: string) {
|
||||
const qc = useQueryClient();
|
||||
const query = useQuery({
|
||||
queryKey: ['conversations', personId],
|
||||
queryFn: () => api.get<ConversationEvent[]>(`/comms/threads/${personId}`),
|
||||
queryFn: async () => {
|
||||
const payload = await api.get<Wrapped<Client360Snapshot>>(`/crm/client-360/${personId}`);
|
||||
return mapConversationEvents(payload.data);
|
||||
},
|
||||
staleTime: 10_000,
|
||||
enabled: !!personId,
|
||||
});
|
||||
@@ -40,7 +46,10 @@ export function useConversations(personId: string) {
|
||||
export function useClientProperties(personId: string) {
|
||||
const query = useQuery({
|
||||
queryKey: ['client-properties', personId],
|
||||
queryFn: () => api.get<PropertyInterest[]>(`/crm/leads/${personId}/properties`),
|
||||
queryFn: async () => {
|
||||
const payload = await api.get<Wrapped<Client360Snapshot>>(`/crm/client-360/${personId}`);
|
||||
return mapPropertyInterests(payload.data);
|
||||
},
|
||||
staleTime: 60_000,
|
||||
enabled: !!personId,
|
||||
});
|
||||
@@ -54,7 +63,10 @@ export function useClientTasks(personId: string) {
|
||||
const qc = useQueryClient();
|
||||
const query = useQuery({
|
||||
queryKey: ['client-tasks', personId],
|
||||
queryFn: () => api.get<Task[]>(`/crm/leads/${personId}/tasks`),
|
||||
queryFn: async () => {
|
||||
const payload = await api.get<Wrapped<Client360Snapshot>>(`/crm/client-360/${personId}`);
|
||||
return mapTasks(payload.data);
|
||||
},
|
||||
staleTime: 30_000,
|
||||
enabled: !!personId,
|
||||
});
|
||||
@@ -131,3 +143,173 @@ interface Task {
|
||||
isDueToday?: boolean;
|
||||
isAIGenerated?: boolean;
|
||||
}
|
||||
|
||||
interface Wrapped<T> {
|
||||
status: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface Client360Snapshot {
|
||||
client_ref?: string;
|
||||
identity?: {
|
||||
person_id?: string;
|
||||
full_name?: string;
|
||||
primary_phone?: string | null;
|
||||
primary_email?: string | null;
|
||||
buyer_type?: string | null;
|
||||
persona_labels?: string[];
|
||||
};
|
||||
current_lead?: {
|
||||
status?: string | null;
|
||||
budget_band?: string | null;
|
||||
urgency?: string | null;
|
||||
objections?: string[];
|
||||
motivations?: string[];
|
||||
};
|
||||
active_opportunities?: Array<{
|
||||
opportunity_id?: string;
|
||||
stage?: string | null;
|
||||
value?: number | null;
|
||||
probability?: number | null;
|
||||
expected_close_date?: string | null;
|
||||
next_action?: string | null;
|
||||
}>;
|
||||
recent_interactions?: Array<{
|
||||
interaction_id?: string;
|
||||
channel?: string | null;
|
||||
interaction_type?: string | null;
|
||||
happened_at?: string | null;
|
||||
summary?: string | null;
|
||||
}>;
|
||||
property_interests?: Array<{
|
||||
interest_id?: string;
|
||||
project_name?: string | null;
|
||||
unit_preference?: string | null;
|
||||
configuration?: string | null;
|
||||
budget_min?: number | null;
|
||||
budget_max?: number | null;
|
||||
priority?: number | null;
|
||||
}>;
|
||||
tasks?: Array<{
|
||||
reminder_id?: string;
|
||||
title?: string | null;
|
||||
notes?: string | null;
|
||||
due_at?: string | null;
|
||||
status?: string | null;
|
||||
created_by_type?: string | null;
|
||||
}>;
|
||||
qd_overview?: Record<string, { current_value?: number | null; computed_at?: string | null; reasoning?: string | null }>;
|
||||
risk_flags?: string[];
|
||||
recommended_next_actions?: string[];
|
||||
}
|
||||
|
||||
function scoreToPercent(value: number | null | undefined): number {
|
||||
const safe = Number(value ?? 0);
|
||||
const pct = safe <= 1 ? safe * 100 : safe;
|
||||
return Math.max(0, Math.min(100, Math.round(pct)));
|
||||
}
|
||||
|
||||
function bestQdScore(snapshot: Client360Snapshot): number {
|
||||
const qd = snapshot.qd_overview ?? {};
|
||||
const preferred = qd.intent_score ?? qd.engagement_score ?? qd.financial_qualification;
|
||||
if (preferred?.current_value != null) return scoreToPercent(preferred.current_value);
|
||||
const first = Object.values(qd).find((item) => item?.current_value != null);
|
||||
return scoreToPercent(first?.current_value);
|
||||
}
|
||||
|
||||
function stageLabel(status?: string | null): string {
|
||||
return (status || 'new').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function stageEmoji(status?: string | null): string {
|
||||
if (!status) return 'N';
|
||||
return status.slice(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
function relativeDate(value?: string | null): string {
|
||||
if (!value) return 'No contact yet';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return 'Recently';
|
||||
const diffMs = Date.now() - date.getTime();
|
||||
const days = Math.floor(diffMs / 86_400_000);
|
||||
if (days > 0) return `${days}d ago`;
|
||||
const hours = Math.floor(diffMs / 3_600_000);
|
||||
if (hours > 0) return `${hours}h ago`;
|
||||
return 'Just now';
|
||||
}
|
||||
|
||||
function mapClient360(snapshot: Client360Snapshot): Client360Data {
|
||||
const identity = snapshot.identity ?? {};
|
||||
const lead = snapshot.current_lead ?? {};
|
||||
const interactions = snapshot.recent_interactions ?? [];
|
||||
const last = interactions[0];
|
||||
const qdScore = bestQdScore(snapshot);
|
||||
const facts: Record<string, string> = {};
|
||||
if (lead.budget_band) facts.budget = lead.budget_band;
|
||||
if (lead.urgency) facts.timeline = lead.urgency;
|
||||
if (identity.buyer_type) facts.buyerType = identity.buyer_type;
|
||||
if (identity.persona_labels?.length) facts.persona = identity.persona_labels.join(', ');
|
||||
|
||||
return {
|
||||
id: identity.person_id || snapshot.client_ref || '',
|
||||
name: identity.full_name || 'Unknown Client',
|
||||
location: snapshot.property_interests?.[0]?.project_name || lead.budget_band || undefined,
|
||||
primaryPhone: identity.primary_phone || undefined,
|
||||
qdScore,
|
||||
qdDelta: 0,
|
||||
stageName: stageLabel(lead.status),
|
||||
stageEmoji: stageEmoji(lead.status),
|
||||
lastContactRelative: relativeDate(last?.happened_at),
|
||||
lastContactChannel: last?.channel || 'crm',
|
||||
aiInsight: snapshot.recommended_next_actions?.[0] || snapshot.active_opportunities?.[0]?.next_action || undefined,
|
||||
extractedFacts: facts,
|
||||
objections: [...(lead.objections ?? []), ...(snapshot.risk_flags ?? [])],
|
||||
qdHistory: Object.entries(snapshot.qd_overview ?? {}).map(([label, item]) => ({
|
||||
date: item.computed_at || new Date().toISOString(),
|
||||
score: scoreToPercent(item.current_value),
|
||||
label: label.replace(/_/g, ' '),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function mapConversationEvents(snapshot: Client360Snapshot): ConversationEvent[] {
|
||||
return (snapshot.recent_interactions ?? []).map((event, index) => {
|
||||
const type = event.channel === 'email' ? 'email' : event.channel === 'call' || event.channel === 'phone_call' ? 'call' : 'whatsapp';
|
||||
return {
|
||||
id: event.interaction_id || `${snapshot.client_ref || 'client'}-${index}`,
|
||||
type,
|
||||
timestamp: event.happened_at || new Date().toISOString(),
|
||||
timestampRelative: relativeDate(event.happened_at),
|
||||
subject: event.interaction_type || undefined,
|
||||
direction: 'outbound',
|
||||
duration: undefined,
|
||||
hasTranscript: false,
|
||||
keyMoments: event.summary ? [event.summary] : [],
|
||||
messages: type === 'whatsapp' ? [{ sender: 'client', text: event.summary || 'Interaction recorded in CRM.' }] : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mapPropertyInterests(snapshot: Client360Snapshot): PropertyInterest[] {
|
||||
return (snapshot.property_interests ?? []).map((item, index) => ({
|
||||
id: item.interest_id || `${snapshot.client_ref || 'property'}-${index}`,
|
||||
projectName: item.project_name || 'Unknown Project',
|
||||
unitName: item.unit_preference || 'Preferred Unit',
|
||||
config: item.configuration || 'Configuration not captured',
|
||||
area: 'Area TBD',
|
||||
price: item.budget_min != null && item.budget_max != null ? `₹${item.budget_min}Cr - ₹${item.budget_max}Cr` : 'Budget TBD',
|
||||
isPrimary: index === 0 || item.priority === 1,
|
||||
engagementLevel: index === 0 ? 'High' : 'Medium',
|
||||
}));
|
||||
}
|
||||
|
||||
function mapTasks(snapshot: Client360Snapshot): Task[] {
|
||||
return (snapshot.tasks ?? []).map((task, index) => ({
|
||||
id: task.reminder_id || `${snapshot.client_ref || 'task'}-${index}`,
|
||||
label: task.title || task.notes || 'Follow up with client',
|
||||
dueAt: task.due_at || undefined,
|
||||
status: task.status === 'done' ? 'done' : task.status === 'snoozed' ? 'snoozed' : 'pending',
|
||||
isDueToday: task.due_at ? new Date(task.due_at).toDateString() === new Date().toDateString() : false,
|
||||
isAIGenerated: task.created_by_type === 'ai',
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user