feat: Complete code integration of modules (#18)

The complete code integration is done.

Co-authored-by: Sagnik <sagnik7896@gmail.com>
Reviewed-on: #18
This commit was merged in pull request #18.
This commit is contained in:
2026-04-12 19:20:14 +05:30
parent 248d92042f
commit 4645ff737b
27 changed files with 3393 additions and 50 deletions

View File

@@ -17,3 +17,119 @@ export const API_URL = (
).replace(/\/$/, '');
export const WS_URL = API_URL.replace(/^http/, 'ws');
export interface ScatterDataPoint {
id: string;
name: string;
sentiment_score: number;
response_time_ms: number;
score: number;
qualification: string;
kanban_status: string;
}
export interface LeadRecord {
id: string;
name: string;
email?: string | null;
phone?: string | null;
source: string;
notes: string;
qualification: string;
score: number;
kanban_status: string;
stage: string;
budget: string;
unit_interest: string;
metadata: Record<string, unknown>;
created_at?: string | null;
updated_at?: string | null;
}
export interface LeadDemographics {
by_source: Array<{ source: string; lead_count: number; avg_score: number }>;
by_qualification: Array<{ qualification: string; lead_count: number }>;
}
export interface ChatLogRecord {
id: string;
lead_id: string;
sender: string;
channel: string;
content: string;
metadata: Record<string, unknown>;
created_at: string | null;
}
export interface MarketingCampaignSummary {
id: string;
name: string;
platform: 'meta' | 'google';
status: 'active' | 'paused' | 'completed';
budget: number;
spent: number;
impressions: number;
clicks: number;
conversions: number;
}
async function requestJson<T>(path: string): Promise<T> {
const response = await fetch(`${API_URL}${path}`, {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json() as Promise<T>;
}
async function requestWrappedData<T>(path: string): Promise<T> {
const payload = await requestJson<{ data: T }>(path);
return payload.data;
}
export async function getSentimentScatter(): Promise<ScatterDataPoint[]> {
return requestJson<ScatterDataPoint[]>('/api/analytics/sentiment-scatter');
}
export async function getCatalystCampaigns(): Promise<MarketingCampaignSummary[]> {
return requestWrappedData<MarketingCampaignSummary[]>('/api/catalyst/campaigns');
}
export async function getLeads(): Promise<LeadRecord[]> {
const payload = await requestJson<{ data: LeadRecord[] }>('/api/leads');
return payload.data;
}
export async function getLead(leadId: string): Promise<LeadRecord> {
return requestWrappedData<LeadRecord>(`/api/leads/${leadId}`);
}
export async function getKanbanBoard() {
return requestWrappedData<Array<{ status: string; stage: string; count: number; items: LeadRecord[] }>>('/api/kanban/board');
}
export async function getChatLogs(leadId?: string): Promise<ChatLogRecord[]> {
const suffix = leadId ? `?lead_id=${encodeURIComponent(leadId)}` : '';
return requestWrappedData<ChatLogRecord[]>(`/api/chat-logs${suffix}`);
}
export async function getLeadDemographics(): Promise<LeadDemographics> {
return requestWrappedData<LeadDemographics>('/api/leads/demographics');
}
export async function seedSyntheticLeads(count = 100): Promise<{ seeded: number; chat_logs_seeded: number; batch: string }> {
const response = await fetch(`${API_URL}/api/leads/seed-synthetic`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ count }),
});
if (!response.ok) {
throw new Error(`Seed request failed: ${response.status}`);
}
const payload = await response.json() as { data: { seeded: number; chat_logs_seeded: number; batch: string } };
return payload.data;
}

122
app/src/lib/crmMappers.ts Normal file
View File

@@ -0,0 +1,122 @@
import type { ChatLogRecord, LeadRecord } from '@/lib/api';
import type { Lead } from '@/types';
import type { LeadBadge, LeadTag, LeadSource, Message, MessageSender, PipelineStage, SentimentLog } from '@/types/crm';
const TAG_MAP: Record<string, LeadTag> = {
whale: '#CashBuyer',
potential: '#Investor',
hot: '#EndUser',
};
export function mapLeadRecordToStoreLead(record: LeadRecord): Lead {
const qualification = record.qualification.toLowerCase() as Lead['qualification'];
const status = record.stage === 'closed'
? 'closed'
: record.stage === 'qualified' || record.stage === 'negotiation'
? 'qualified'
: record.score >= 75
? 'hot'
: record.stage === 'new'
? 'new'
: 'engaged';
const tags = Array.isArray(record.metadata?.tags) ? (record.metadata.tags as string[]) : [];
return {
id: record.id,
name: record.name,
phone: record.phone ?? '',
source: mapSource(record.source),
status,
lastMessage: record.notes || 'No conversation summary yet.',
lastActive: new Date(record.updated_at ?? record.created_at ?? Date.now()),
unreadCount: 0,
qualification: qualification === 'tire_kicker' || qualification === 'potential' || qualification === 'whale'
? qualification
: 'potential',
budget: record.budget,
interest: record.unit_interest,
quantumDynamicsScore: record.score,
tags: tags.length > 0 ? tags : [record.qualification],
};
}
export function mapLeadRecordToOracleLead(record: LeadRecord, chatLogs: ChatLogRecord[]): import('@/types/crm').Lead {
const badge = mapBadge(record.qualification);
const tags = mapOracleTags(record.qualification, record.metadata);
return {
id: record.id,
name: record.name,
phone: record.phone ?? '',
stage: mapPipelineStage(record.stage),
oracleScore: record.score,
badge,
tags,
source: mapSource(record.source),
budget: record.budget,
unitInterest: record.unit_interest,
profileImageUrl: `https://api.dicebear.com/9.x/glass/svg?seed=${encodeURIComponent(record.name)}`,
visitedShowroom: record.stage === 'site_visit' || record.stage === 'negotiation' || record.stage === 'closed',
inShowroomNow: record.stage === 'site_visit',
messages: chatLogs.map(mapChatLogToOracleMessage),
sentimentLog: buildSentimentLog(record.score, record.stage),
};
}
function mapSource(source: string): LeadSource {
if (source === 'walkin' || source === 'website' || source === 'whatsapp') return source;
return 'website';
}
function mapPipelineStage(stage: string): PipelineStage {
const normalized = stage.toLowerCase();
if (normalized === 'new' || normalized === 'new_inquiries') return 'new_inquiries';
if (normalized === 'qualified' || normalized === 'qualifying') return 'qualified';
if (normalized === 'site_visit') return 'site_visit';
if (normalized === 'negotiation') return 'negotiation';
return 'closed';
}
function mapBadge(qualification: string): LeadBadge | undefined {
const normalized = qualification.toLowerCase();
if (normalized === 'whale') return 'whale';
if (normalized === 'hot' || normalized === 'potential') return 'hot';
if (normalized === 'tire_kicker') return 'tire_kicker';
return undefined;
}
function mapOracleTags(qualification: string, metadata: Record<string, unknown>): LeadTag[] {
const mapped = TAG_MAP[qualification.toLowerCase()];
const rawTags = Array.isArray(metadata?.tags) ? metadata.tags as string[] : [];
const canonical = rawTags.includes('#CashBuyer') || mapped === '#CashBuyer'
? '#CashBuyer'
: rawTags.includes('#EndUser') || mapped === '#EndUser'
? '#EndUser'
: '#Investor';
return [canonical];
}
function mapChatLogToOracleMessage(log: ChatLogRecord): Message {
return {
id: log.id,
sender: mapSender(log.sender),
content: log.content,
createdAt: log.created_at ?? new Date().toISOString(),
};
}
function mapSender(sender: string): MessageSender {
if (sender === 'lead' || sender === 'oracle' || sender === 'system') return sender;
return 'system';
}
function buildSentimentLog(score: number, stage: string): SentimentLog[] {
const base = Math.max(20, score - 18);
const labels = stage === 'site_visit'
? ['Entry', 'Showroom peak', 'Pricing review']
: ['Discovery', 'Qualification', 'Follow-up'];
return labels.map((label, index) => ({
id: `${stage}-${index}`,
at: `${10 + index}:0${index}`,
score: Math.min(100, base + index * 9),
note: label,
}));
}