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

This commit is contained in:
2026-05-02 12:13:09 +05:30
parent ce02ac958b
commit 08a19db035
8 changed files with 455 additions and 67 deletions

View File

@@ -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',
}));
}