import { NextResponse } from 'next/server'; import { getSession } from '@/lib/auth'; import { createMCPClient } from '@/lib/control-center/mcp-client'; export interface Recommendation { id: string; type: 'follow_up' | 'stale_lead' | 'pipeline_stuck' | 'campaign_response' | 'no_response'; priority: 'high' | 'medium' | 'low'; title: string; description: string; actionLabel: string; actionUrl: string; contactId?: string; contactName?: string; daysOverdue?: number; pipelineStage?: string; } interface RecommendationsResponse { recommendations: Recommendation[]; generatedAt: string; } // Fallback recommendations shown when MCP isn't available or returns no data function getFallbackRecommendations(): Recommendation[] { return [ { id: 'fallback-import-contacts', type: 'follow_up', priority: 'high', title: 'Import your contacts', description: 'Get started by importing your existing contacts from a CSV or CRM', actionLabel: 'Import Now', actionUrl: '/contacts', }, { id: 'fallback-configure-sms', type: 'follow_up', priority: 'medium', title: 'Set up SMS messaging', description: 'Configure two-way SMS to reach contacts via text', actionLabel: 'Configure', actionUrl: '/settings/sms', }, { id: 'fallback-create-pipeline', type: 'pipeline_stuck', priority: 'medium', title: 'Create your first pipeline', description: 'Track deals from lead to close with custom stages', actionLabel: 'Create Pipeline', actionUrl: '/opportunities', }, { id: 'fallback-explore-control-center', type: 'campaign_response', priority: 'low', title: 'Explore AI Control Center', description: 'Use AI to analyze your CRM data and get insights', actionLabel: 'Explore', actionUrl: '/control-center', }, ]; } export async function GET() { const session = await getSession(); if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { const mcpClient = await createMCPClient(); if (!mcpClient) { // Return fallback recommendations if MCP not configured return NextResponse.json({ recommendations: getFallbackRecommendations(), generatedAt: new Date().toISOString(), }); } const isHealthy = await mcpClient.healthCheck(); if (!isHealthy) { return NextResponse.json({ recommendations: getFallbackRecommendations(), generatedAt: new Date().toISOString(), }); } // Ensure connected for tool execution await mcpClient.connect(); const recommendations: Recommendation[] = []; // 1. Get conversations and check for ones needing follow-up try { const conversationsResult = await mcpClient.executeTool('search_conversations', { limit: 50, }); if (conversationsResult.success && conversationsResult.result) { const data = conversationsResult.result as any; const conversations = data?.conversations || data || []; for (const conv of (Array.isArray(conversations) ? conversations : []).slice(0, 15)) { // Check if last message was from us and no response const lastMessageAt = conv.lastMessageDate || conv.dateUpdated || conv.updatedAt; const lastDate = lastMessageAt ? new Date(lastMessageAt) : null; const daysSinceMessage = lastDate ? Math.floor((Date.now() - lastDate.getTime()) / (1000 * 60 * 60 * 24)) : null; const lastMessageDirection = conv.lastMessageDirection || conv.lastMessageType || ''; const isOutbound = lastMessageDirection === 'outbound' || lastMessageDirection === 'out'; if (daysSinceMessage && daysSinceMessage >= 7 && isOutbound) { const contactName = conv.contactName || conv.fullName || conv.name || 'contact'; recommendations.push({ id: `no-response-${conv.id}`, type: 'no_response', priority: daysSinceMessage >= 14 ? 'high' : 'medium', title: `Follow up with ${contactName}`, description: `No response in ${daysSinceMessage} days since your last message`, actionLabel: 'Send Follow-up', actionUrl: `/conversations/${conv.id}`, contactId: conv.contactId, contactName: contactName, daysOverdue: daysSinceMessage, }); } } } } catch (e) { console.warn('[Recommendations] Failed to fetch conversations:', e); } // 2. Get opportunities stuck in pipeline stages try { const oppsResult = await mcpClient.executeTool('search_opportunities', { status: 'open', limit: 30, }); if (oppsResult.success && oppsResult.result) { const data = oppsResult.result as any; const opportunities = data?.opportunities || data || []; for (const opp of (Array.isArray(opportunities) ? opportunities : []).slice(0, 20)) { const stageEnteredAt = opp.lastStageChangeAt || opp.stageEnteredAt || opp.updatedAt || opp.dateUpdated; const stageDate = stageEnteredAt ? new Date(stageEnteredAt) : null; const daysInStage = stageDate ? Math.floor((Date.now() - stageDate.getTime()) / (1000 * 60 * 60 * 24)) : null; // Flag opportunities stuck in same stage for 14+ days if (daysInStage && daysInStage >= 14) { const oppName = opp.name || opp.title || 'Opportunity'; const stageName = opp.pipelineStage || opp.stageName || opp.stage || 'current stage'; recommendations.push({ id: `pipeline-stuck-${opp.id}`, type: 'pipeline_stuck', priority: daysInStage >= 30 ? 'high' : 'medium', title: `Move "${oppName}" forward`, description: `Stuck in "${stageName}" for ${daysInStage} days`, actionLabel: 'Review Deal', actionUrl: `/opportunities/${opp.id}`, contactId: opp.contactId, contactName: opp.contactName || opp.contact?.name, daysOverdue: daysInStage, pipelineStage: stageName, }); } } } } catch (e) { console.warn('[Recommendations] Failed to fetch opportunities:', e); } // 3. Get email campaigns (optional - may not have data) try { const campaignsResult = await mcpClient.executeTool('get_email_campaigns', { limit: 10, }); if (campaignsResult.success && campaignsResult.result) { const data = campaignsResult.result as any; const campaigns = data?.campaigns || data || []; for (const campaign of (Array.isArray(campaigns) ? campaigns : []).slice(0, 5)) { // Check for campaigns with good open/click rates that might need follow-up const opens = campaign.opens || campaign.openCount || 0; const clicks = campaign.clicks || campaign.clickCount || 0; if (opens > 0 || clicks > 0) { recommendations.push({ id: `campaign-engagement-${campaign.id}`, type: 'campaign_response', priority: clicks > 5 ? 'high' : 'medium', title: `"${campaign.name || 'Campaign'}" has engagement`, description: `${opens} opens, ${clicks} clicks - follow up with engaged leads`, actionLabel: 'View Campaign', actionUrl: `/campaigns/${campaign.id}`, }); } } } } catch (e) { console.warn('[Recommendations] Failed to fetch campaigns:', e); } // 4. Get contacts and find ones needing outreach try { const contactsResult = await mcpClient.executeTool('search_contacts', { limit: 30, }); if (contactsResult.success && contactsResult.result) { const data = contactsResult.result as any; const contacts = data?.contacts || data || []; for (const contact of (Array.isArray(contacts) ? contacts : []).slice(0, 20)) { const dateAdded = contact.dateAdded || contact.createdAt || contact.dateCreated; const addedDate = dateAdded ? new Date(dateAdded) : null; const daysSinceAdded = addedDate ? Math.floor((Date.now() - addedDate.getTime()) / (1000 * 60 * 60 * 24)) : null; // Check if contact was added in last 14 days and has no conversation const hasConversation = contact.lastMessageDate || contact.lastConversationDate; if (daysSinceAdded !== null && daysSinceAdded <= 14 && !hasConversation) { const firstName = contact.firstName || contact.name?.split(' ')[0] || ''; const lastName = contact.lastName || ''; const displayName = firstName || contact.email || 'new contact'; recommendations.push({ id: `new-contact-${contact.id}`, type: 'follow_up', priority: daysSinceAdded <= 3 ? 'high' : 'medium', title: `Reach out to ${displayName}`, description: `Added ${getRelativeTime(dateAdded)} - no outreach yet`, actionLabel: 'Start Conversation', actionUrl: `/contacts/${contact.id}`, contactId: contact.id, contactName: firstName ? `${firstName} ${lastName}`.trim() : contact.email, }); } } } } catch (e) { console.warn('[Recommendations] Failed to fetch contacts:', e); } // Sort by priority and limit to top 4 const priorityOrder = { high: 0, medium: 1, low: 2 }; recommendations.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); const topRecommendations = recommendations.slice(0, 4); // If no recommendations from MCP data, return fallback recommendations const finalRecommendations = topRecommendations.length > 0 ? topRecommendations : getFallbackRecommendations(); const response: RecommendationsResponse = { recommendations: finalRecommendations, generatedAt: new Date().toISOString(), }; return NextResponse.json(response); } catch (error) { console.error('[Recommendations] Error:', error); // Return fallback recommendations on error instead of failing return NextResponse.json({ recommendations: getFallbackRecommendations(), generatedAt: new Date().toISOString(), }); } } function getRelativeTime(dateString: string | number | undefined): string { if (!dateString) return 'recently'; const date = typeof dateString === 'number' ? new Date(dateString * 1000) : new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) return 'today'; if (diffDays === 1) return 'yesterday'; if (diffDays < 7) return `${diffDays} days ago`; if (diffDays < 14) return 'last week'; return `${Math.floor(diffDays / 7)} weeks ago`; }