- /api/network/matches: find all client matches with scoring - /api/network/matches/:clientId: matches for a specific client - /api/network/intro: AI-generated introduction suggestions - /api/network/stats: network analytics (industries, locations, connectors) - Rule-based scoring: industry, interests, location, tags, complementary roles - Smart filtering: same company detection, related industry groups
166 lines
5.6 KiB
TypeScript
166 lines
5.6 KiB
TypeScript
import { Elysia, t } from 'elysia';
|
|
import { db } from '../db';
|
|
import { clients } from '../db/schema';
|
|
import { eq } from 'drizzle-orm';
|
|
import { findMatches, findMatchesForClient, generateIntroSuggestion } from '../services/matching';
|
|
import type { ClientProfile, MatchResult } from '../services/matching';
|
|
|
|
function toClientProfile(c: typeof clients.$inferSelect): ClientProfile {
|
|
return {
|
|
id: c.id,
|
|
firstName: c.firstName,
|
|
lastName: c.lastName,
|
|
email: c.email,
|
|
company: c.company,
|
|
role: c.role,
|
|
industry: c.industry,
|
|
city: c.city,
|
|
state: c.state,
|
|
interests: c.interests,
|
|
tags: c.tags,
|
|
notes: c.notes,
|
|
};
|
|
}
|
|
|
|
export const networkRoutes = new Elysia({ prefix: '/network' })
|
|
// Get all network matches for the user's clients
|
|
.get('/matches', async (ctx) => {
|
|
const user = (ctx as any).user;
|
|
const query = ctx.query;
|
|
const allClients = await db.select().from(clients).where(eq(clients.userId, user.id));
|
|
|
|
if (allClients.length < 2) {
|
|
return { matches: [] as MatchResult[], total: 0, clientCount: allClients.length };
|
|
}
|
|
|
|
const profiles = allClients.map(toClientProfile);
|
|
const minScore = parseInt(query.minScore || '20', 10);
|
|
const limit = parseInt(query.limit || '50', 10);
|
|
|
|
const rawMatches = findMatches(profiles, minScore).slice(0, limit);
|
|
|
|
const matches: MatchResult[] = rawMatches.map(m => ({
|
|
...m,
|
|
introSuggestion: `Consider introducing ${m.clientA.name} and ${m.clientB.name} — they share common ground.`,
|
|
}));
|
|
|
|
return { matches, total: matches.length, clientCount: allClients.length };
|
|
}, {
|
|
query: t.Object({
|
|
minScore: t.Optional(t.String()),
|
|
limit: t.Optional(t.String()),
|
|
}),
|
|
})
|
|
|
|
// Get matches for a specific client
|
|
.get('/matches/:clientId', async (ctx) => {
|
|
const user = (ctx as any).user;
|
|
const params = ctx.params;
|
|
const query = ctx.query;
|
|
const allClients = await db.select().from(clients).where(eq(clients.userId, user.id));
|
|
const target = allClients.find(c => c.id === params.clientId);
|
|
|
|
if (!target) throw new Error('Client not found');
|
|
|
|
const profiles = allClients.map(toClientProfile);
|
|
const targetProfile = toClientProfile(target);
|
|
const minScore = parseInt(query.minScore || '15', 10);
|
|
|
|
const rawMatches = findMatchesForClient(targetProfile, profiles, minScore);
|
|
|
|
const matches: MatchResult[] = rawMatches.map(m => ({
|
|
...m,
|
|
introSuggestion: `Consider introducing ${m.clientA.name} and ${m.clientB.name} — they share common ground.`,
|
|
}));
|
|
|
|
return { matches, client: { id: target.id, name: `${target.firstName} ${target.lastName}` } };
|
|
}, {
|
|
params: t.Object({
|
|
clientId: t.String({ format: 'uuid' }),
|
|
}),
|
|
query: t.Object({
|
|
minScore: t.Optional(t.String()),
|
|
}),
|
|
})
|
|
|
|
// Generate AI intro suggestion for a specific match
|
|
.post('/intro', async (ctx) => {
|
|
const user = (ctx as any).user;
|
|
const body = ctx.body;
|
|
const allClients = await db.select().from(clients).where(eq(clients.userId, user.id));
|
|
const clientA = allClients.find(c => c.id === body.clientAId);
|
|
const clientB = allClients.find(c => c.id === body.clientBId);
|
|
|
|
if (!clientA || !clientB) throw new Error('Client not found');
|
|
|
|
const suggestion = await generateIntroSuggestion(
|
|
toClientProfile(clientA),
|
|
toClientProfile(clientB),
|
|
body.reasons,
|
|
(body.provider as any) || 'openai',
|
|
);
|
|
|
|
return { introSuggestion: suggestion };
|
|
}, {
|
|
body: t.Object({
|
|
clientAId: t.String({ format: 'uuid' }),
|
|
clientBId: t.String({ format: 'uuid' }),
|
|
reasons: t.Array(t.String()),
|
|
provider: t.Optional(t.String()),
|
|
}),
|
|
})
|
|
|
|
// Network stats
|
|
.get('/stats', async (ctx) => {
|
|
const user = (ctx as any).user;
|
|
const allClients = await db.select().from(clients).where(eq(clients.userId, user.id));
|
|
const profiles = allClients.map(toClientProfile);
|
|
const matches = findMatches(profiles, 20);
|
|
|
|
// Industry breakdown
|
|
const industries: Record<string, number> = {};
|
|
for (const c of allClients) {
|
|
if (c.industry) {
|
|
industries[c.industry] = (industries[c.industry] || 0) + 1;
|
|
}
|
|
}
|
|
|
|
// Location breakdown
|
|
const locations: Record<string, number> = {};
|
|
for (const c of allClients) {
|
|
const loc = [c.city, c.state].filter(Boolean).join(', ');
|
|
if (loc) {
|
|
locations[loc] = (locations[loc] || 0) + 1;
|
|
}
|
|
}
|
|
|
|
// Category breakdown
|
|
const categories: Record<string, number> = {};
|
|
for (const m of matches) {
|
|
categories[m.category] = (categories[m.category] || 0) + 1;
|
|
}
|
|
|
|
// Most connected clients
|
|
const connectionCount: Record<string, { name: string; count: number }> = {};
|
|
for (const m of matches) {
|
|
if (!connectionCount[m.clientA.id]) connectionCount[m.clientA.id] = { name: m.clientA.name, count: 0 };
|
|
if (!connectionCount[m.clientB.id]) connectionCount[m.clientB.id] = { name: m.clientB.name, count: 0 };
|
|
connectionCount[m.clientA.id]!.count++;
|
|
connectionCount[m.clientB.id]!.count++;
|
|
}
|
|
const topConnectors = Object.entries(connectionCount)
|
|
.map(([id, { name, count }]) => ({ id, name, matchCount: count }))
|
|
.sort((a, b) => b.matchCount - a.matchCount)
|
|
.slice(0, 5);
|
|
|
|
return {
|
|
totalClients: allClients.length,
|
|
totalMatches: matches.length,
|
|
avgScore: matches.length > 0 ? Math.round(matches.reduce((s, m) => s + m.score, 0) / matches.length) : 0,
|
|
industries: Object.entries(industries).sort((a, b) => b[1] - a[1]),
|
|
locations: Object.entries(locations).sort((a, b) => b[1] - a[1]),
|
|
categories,
|
|
topConnectors,
|
|
};
|
|
});
|