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:
2026-04-18 18:59:04 +05:30
parent 857e0b88e6
commit 84e439712c
459 changed files with 11713 additions and 3853 deletions

View File

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