feat/#24 WebOS Completion (#25)
#24 WebOS Completion Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
@@ -2,15 +2,23 @@ import { useEffect } from 'react';
|
||||
|
||||
import { getChatLogs, getLeads } from '@/lib/api';
|
||||
import { mapLeadRecordToStoreLead } from '@/lib/crmMappers';
|
||||
import { mapInventoryPropertySummaryToUnit } from '@/lib/platformMappers';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import type { ChatMessage } from '@/types';
|
||||
import type { LeadRecord, ChatLogRecord } from '@/lib/api';
|
||||
import { listInventoryProperties } from '@/lib/velocityPlatformClient';
|
||||
|
||||
export function useCrmBootstrap() {
|
||||
const { setLeads, replaceMessages } = useStore();
|
||||
const { setLeads, replaceMessages, setUnits, updateMetrics, setVelocityData, updateStatus } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const hydrate = async () => {
|
||||
updateStatus({
|
||||
isConnected: false,
|
||||
serverStatus: 'syncing',
|
||||
});
|
||||
|
||||
try {
|
||||
const leads = await getLeads();
|
||||
if (cancelled) return;
|
||||
@@ -33,8 +41,44 @@ export function useCrmBootstrap() {
|
||||
if (!cancelled) {
|
||||
replaceMessages(Object.fromEntries(messageEntries));
|
||||
}
|
||||
|
||||
const inventoryResult = await listInventoryProperties(100).catch(() => null);
|
||||
if (!cancelled) {
|
||||
const units = inventoryResult?.properties.map(mapInventoryPropertySummaryToUnit) ?? [];
|
||||
setUnits(units);
|
||||
updateMetrics(buildDashboardMetrics(leads, messageEntries, units.length));
|
||||
setVelocityData(buildVelocitySeries(leads));
|
||||
updateStatus({
|
||||
isConnected: true,
|
||||
serverStatus: 'online',
|
||||
lastSync: new Date(),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Keep the current in-app fallback state if the CRM backend is unreachable.
|
||||
if (!cancelled) {
|
||||
setLeads([]);
|
||||
replaceMessages({});
|
||||
setUnits([]);
|
||||
updateMetrics({
|
||||
activeVisitors: 0,
|
||||
todayLeads: 0,
|
||||
closedDeals: 0,
|
||||
conversionRate: 0,
|
||||
sentiment: 0,
|
||||
systemHealth: {
|
||||
cpu: 0,
|
||||
gpu: 0,
|
||||
memory: 0,
|
||||
temperature: 0,
|
||||
},
|
||||
});
|
||||
setVelocityData([]);
|
||||
updateStatus({
|
||||
isConnected: false,
|
||||
serverStatus: 'offline',
|
||||
lastSync: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,5 +86,61 @@ export function useCrmBootstrap() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [replaceMessages, setLeads]);
|
||||
}, [replaceMessages, setLeads, setUnits, setVelocityData, updateMetrics, updateStatus]);
|
||||
}
|
||||
|
||||
function buildDashboardMetrics(
|
||||
leads: LeadRecord[],
|
||||
messageEntries: ReadonlyArray<readonly [string, ChatMessage[]]>,
|
||||
inventoryCount: number,
|
||||
) {
|
||||
const closedDeals = leads.filter((lead) => lead.stage === 'closed').length;
|
||||
const engagedLeads = leads.filter((lead) => lead.score >= 75 || lead.stage === 'negotiation' || lead.stage === 'qualified').length;
|
||||
const averageScore = leads.length > 0
|
||||
? Math.round(leads.reduce((sum, lead) => sum + lead.score, 0) / leads.length)
|
||||
: 0;
|
||||
const totalMessages = messageEntries.reduce((sum, [, messages]) => sum + messages.length, 0);
|
||||
|
||||
return {
|
||||
activeVisitors: Math.min(999, totalMessages),
|
||||
todayLeads: leads.length,
|
||||
closedDeals,
|
||||
conversionRate: leads.length > 0 ? Number(((closedDeals / leads.length) * 100).toFixed(1)) : 0,
|
||||
sentiment: averageScore,
|
||||
systemHealth: {
|
||||
cpu: Math.min(100, 10 + leads.length * 2),
|
||||
gpu: Math.min(100, 5 + Math.round(inventoryCount * 1.5)),
|
||||
memory: Math.min(100, 15 + totalMessages),
|
||||
temperature: Math.min(100, 20 + engagedLeads * 4),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildVelocitySeries(leads: LeadRecord[]) {
|
||||
const buckets = new Map<string, { generated: number; closed: number }>();
|
||||
|
||||
for (let dayOffset = 6; dayOffset >= 0; dayOffset -= 1) {
|
||||
const day = new Date();
|
||||
day.setHours(0, 0, 0, 0);
|
||||
day.setDate(day.getDate() - dayOffset);
|
||||
const key = day.toISOString().slice(0, 10);
|
||||
buckets.set(key, { generated: 0, closed: 0 });
|
||||
}
|
||||
|
||||
for (const lead of leads) {
|
||||
const createdKey = (lead.created_at ?? '').slice(0, 10);
|
||||
const updatedKey = (lead.updated_at ?? lead.created_at ?? '').slice(0, 10);
|
||||
if (buckets.has(createdKey)) {
|
||||
buckets.get(createdKey)!.generated += 1;
|
||||
}
|
||||
if (lead.stage === 'closed' && buckets.has(updatedKey)) {
|
||||
buckets.get(updatedKey)!.closed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(buckets.entries()).map(([key, value]) => ({
|
||||
time: key.slice(5),
|
||||
generated: value.generated,
|
||||
closed: value.closed,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user