UNPKG

jobnimbus-mcp-client

Version:

JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server

452 lines 24.1 kB
/** * Get Sales Velocity Analytics * Comprehensive sales velocity tracking with win rate analysis, sales cycle duration, pipeline acceleration metrics, and performance optimization */ import { BaseTool } from '../baseTool.js'; export class GetSalesVelocityAnalyticsTool extends BaseTool { get definition() { return { name: 'get_sales_velocity_analytics', description: 'Comprehensive sales velocity analytics with win rate analysis, sales cycle duration tracking, pipeline acceleration metrics, rep performance, and velocity optimization recommendations', inputSchema: { type: 'object', properties: { time_window_days: { type: 'number', default: 90, description: 'Days to analyze (default: 90)', }, include_rep_analysis: { type: 'boolean', default: true, description: 'Include sales rep velocity analysis', }, include_deal_segmentation: { type: 'boolean', default: true, description: 'Include deal size segmentation', }, min_deal_size: { type: 'number', default: 0, description: 'Minimum deal size to include', }, }, }, }; } async execute(input, context) { try { const timeWindowDays = input.time_window_days || 90; const includeRepAnalysis = input.include_rep_analysis !== false; const includeDealSegmentation = input.include_deal_segmentation !== false; const minDealSize = input.min_deal_size || 0; const [jobsResponse] = await Promise.all([ this.client.get(context.apiKey, 'jobs', { size: 100 }), this.client.get(context.apiKey, 'contacts', { size: 100 }), ]); const jobs = jobsResponse.data?.results || []; // const contacts = contactsResponse.data?.results || []; // Try to fetch users - endpoint may not be available in all JobNimbus accounts let users = []; try { const usersResponse = await this.client.get(context.apiKey, 'users', { size: 100 }); users = usersResponse.data?.results || []; } catch (error) { // Users endpoint not available - proceed without user attribution console.warn('Users endpoint not available - sales rep analysis will use job.sales_rep_name'); } const now = Date.now(); const cutoffDate = now - (timeWindowDays * 24 * 60 * 60 * 1000); // Sales cycle tracking const deals = jobs.filter((job) => { const value = parseFloat(job.total || job.value || 0); return value >= minDealSize; }); // Categorize deals const wonDeals = deals.filter((job) => { const status = (job.status_name || '').toLowerCase(); const completedDate = job.date_status_change || job.date_updated || 0; return (status.includes('complete') || status.includes('won')) && completedDate >= cutoffDate; }); const lostDeals = deals.filter((job) => { const status = (job.status_name || '').toLowerCase(); return status.includes('lost') || status.includes('cancelled') || status.includes('rejected'); }); const activeDeals = deals.filter((job) => { const status = (job.status_name || '').toLowerCase(); return !status.includes('complete') && !status.includes('won') && !status.includes('lost') && !status.includes('cancelled'); }); // Calculate sales cycle duration const cycleTimesWon = []; let totalRevenueWon = 0; for (const job of wonDeals) { const createdDate = job.date_created || 0; const closedDate = job.date_status_change || job.date_updated || 0; if (createdDate > 0 && closedDate > 0) { const cycleDays = Math.max(1, Math.floor((closedDate - createdDate) / (24 * 60 * 60 * 1000))); cycleTimesWon.push(cycleDays); totalRevenueWon += parseFloat(job.total || job.value || 0); } } const avgSalesCycle = cycleTimesWon.length > 0 ? cycleTimesWon.reduce((sum, days) => sum + days, 0) / cycleTimesWon.length : 30; const avgDealSize = wonDeals.length > 0 ? totalRevenueWon / wonDeals.length : 0; // Win rate const totalOpportunities = wonDeals.length + lostDeals.length; const winRate = totalOpportunities > 0 ? (wonDeals.length / totalOpportunities) * 100 : 0; // Sales velocity: (Number of Deals × Average Deal Size × Win Rate) / Sales Cycle Length const dealsPerMonth = (wonDeals.length / timeWindowDays) * 30; const monthlyVelocity = (dealsPerMonth * avgDealSize * (winRate / 100)) / (avgSalesCycle / 30); const overallVelocity = totalRevenueWon / Math.max(avgSalesCycle, 1); // Velocity trend (compare first half vs second half) const midpoint = cutoffDate + ((now - cutoffDate) / 2); const firstHalfDeals = wonDeals.filter((j) => (j.date_status_change || j.date_updated || 0) < midpoint); const secondHalfDeals = wonDeals.filter((j) => (j.date_status_change || j.date_updated || 0) >= midpoint); const firstHalfVelocity = firstHalfDeals.length / Math.max(avgSalesCycle, 1); const secondHalfVelocity = secondHalfDeals.length / Math.max(avgSalesCycle, 1); const velocityTrend = secondHalfVelocity > firstHalfVelocity * 1.1 ? 'Accelerating' : secondHalfVelocity < firstHalfVelocity * 0.9 ? 'Decelerating' : 'Stable'; const velocityScore = Math.min((winRate / 50) * 40 + (Math.min(dealsPerMonth, 20) / 20) * 30 + (Math.max(0, 60 - avgSalesCycle) / 60) * 30, 100); const salesVelocityMetrics = { overall_velocity: overallVelocity, monthly_velocity: monthlyVelocity, deals_per_month: dealsPerMonth, avg_deal_size: avgDealSize, win_rate: winRate, avg_sales_cycle_days: avgSalesCycle, velocity_trend: velocityTrend, velocity_score: velocityScore, }; // Sales cycle analysis by stage const stageMap = new Map(); for (const job of deals) { const stage = job.status_name || 'Unknown'; if (!stageMap.has(stage)) { stageMap.set(stage, { durations: [], deals: 0, conversions: 0 }); } const stageData = stageMap.get(stage); stageData.deals++; const createdDate = job.date_created || 0; const updatedDate = job.date_updated || 0; if (createdDate > 0 && updatedDate > 0) { const duration = Math.max(1, Math.floor((updatedDate - createdDate) / (24 * 60 * 60 * 1000))); stageData.durations.push(duration); } } const salesCycleAnalyses = []; for (const [stage, data] of stageMap.entries()) { const avgDuration = data.durations.length > 0 ? data.durations.reduce((sum, d) => sum + d, 0) / data.durations.length : 0; const conversionRate = totalOpportunities > 0 ? (data.deals / totalOpportunities) * 100 : 0; const bottleneckSeverity = avgDuration > 60 ? 'Severe' : avgDuration > 30 ? 'Moderate' : avgDuration > 14 ? 'Minor' : 'None'; const accelerationOpportunity = avgDuration > 30 ? Math.min((avgDuration - 30) / avgDuration * 100, 100) : 0; const recommendedActions = []; if (bottleneckSeverity !== 'None') { recommendedActions.push('Streamline approval process'); recommendedActions.push('Add automation triggers'); if (avgDuration > 45) recommendedActions.push('Escalate stuck deals weekly'); } salesCycleAnalyses.push({ stage, avg_duration_days: avgDuration, deals_in_stage: data.deals, conversion_rate: conversionRate, bottleneck_severity: bottleneckSeverity, acceleration_opportunity: accelerationOpportunity, recommended_actions: recommendedActions, }); } // Win/Loss analysis const winReasons = new Map(); const lossReasons = new Map(); // Infer reasons from job types and notes (simplified) wonDeals.forEach((job) => { const jobType = job.job_type || 'Standard'; winReasons.set(jobType, (winReasons.get(jobType) || 0) + 1); }); lostDeals.forEach((job) => { const status = job.status_name || 'Unknown'; lossReasons.set(status, (lossReasons.get(status) || 0) + 1); }); const topWinReasons = Array.from(winReasons.entries()) .map(([reason, count]) => ({ reason, count, percentage: (count / wonDeals.length) * 100, })) .sort((a, b) => b.count - a.count) .slice(0, 5); const topLossReasons = Array.from(lossReasons.entries()) .map(([reason, count]) => ({ reason, count, percentage: (count / lostDeals.length) * 100, })) .sort((a, b) => b.count - a.count) .slice(0, 5); const avgWinSize = wonDeals.length > 0 ? wonDeals.reduce((sum, j) => sum + parseFloat(j.total || j.value || 0), 0) / wonDeals.length : 0; const avgLossSize = lostDeals.length > 0 ? lostDeals.reduce((sum, j) => sum + parseFloat(j.total || j.value || 0), 0) / lostDeals.length : 0; const winLossAnalysis = { total_opportunities: totalOpportunities, won_deals: wonDeals.length, lost_deals: lostDeals.length, active_deals: activeDeals.length, win_rate: winRate, loss_rate: totalOpportunities > 0 ? (lostDeals.length / totalOpportunities) * 100 : 0, avg_win_size: avgWinSize, avg_loss_size: avgLossSize, win_loss_ratio: lostDeals.length > 0 ? wonDeals.length / lostDeals.length : wonDeals.length, top_win_reasons: topWinReasons, top_loss_reasons: topLossReasons, }; // Pipeline acceleration opportunities const pipelineAccelerations = [ { metric_name: 'Sales Cycle Duration', current_value: avgSalesCycle, target_value: 30, gap: Math.max(0, avgSalesCycle - 30), impact_on_velocity: avgSalesCycle > 30 ? ((avgSalesCycle - 30) / avgSalesCycle) * 100 : 0, priority: avgSalesCycle > 60 ? 'Critical' : avgSalesCycle > 45 ? 'High' : 'Medium', acceleration_tactics: [ 'Implement automated follow-up sequences', 'Add sales enablement content library', 'Create proposal templates', ], estimated_improvement: `${Math.min(30, (avgSalesCycle - 30))} days faster`, }, { metric_name: 'Win Rate', current_value: winRate, target_value: 40, gap: Math.max(0, 40 - winRate), impact_on_velocity: winRate < 40 ? ((40 - winRate) / 40) * 100 : 0, priority: winRate < 25 ? 'Critical' : winRate < 35 ? 'High' : 'Medium', acceleration_tactics: [ 'Improve lead qualification (BANT/MEDDIC)', 'Enhance discovery call framework', 'Add competitive battle cards', ], estimated_improvement: `+${Math.min(15, (40 - winRate)).toFixed(1)}% win rate`, }, ]; // Sales rep velocity const repVelocities = []; if (includeRepAnalysis) { const repMap = new Map(); for (const job of wonDeals) { const repId = job.assigned_user_id || job.sales_rep_id || 'Unassigned'; if (!repMap.has(repId)) { repMap.set(repId, { deals: [], revenue: 0 }); } const repData = repMap.get(repId); repData.deals.push(job); repData.revenue += parseFloat(job.total || job.value || 0); } for (const [repId, data] of repMap.entries()) { const user = users.find((u) => u.id === repId); const repName = user ? `${user.first_name || ''} ${user.last_name || ''}`.trim() : repId; const repCycleTimes = data.deals .map((j) => { const created = j.date_created || 0; const closed = j.date_status_change || j.date_updated || 0; return created > 0 && closed > 0 ? (closed - created) / (24 * 60 * 60 * 1000) : 0; }) .filter((d) => d > 0); const repAvgCycle = repCycleTimes.length > 0 ? repCycleTimes.reduce((sum, d) => sum + d, 0) / repCycleTimes.length : avgSalesCycle; const repAvgDeal = data.deals.length > 0 ? data.revenue / data.deals.length : 0; const repVelocity = data.revenue / Math.max(repAvgCycle, 1); const repVelocityScore = Math.min((data.deals.length / Math.max(wonDeals.length, 1)) * 50 + (repVelocity / Math.max(overallVelocity, 1)) * 50, 100); const performanceRating = repVelocityScore >= 80 ? 'Excellent' : repVelocityScore >= 60 ? 'Good' : repVelocityScore >= 40 ? 'Fair' : 'Needs Improvement'; const recommendedCoaching = []; if (repAvgCycle > avgSalesCycle * 1.2) { recommendedCoaching.push('Focus on accelerating deal closure'); } if (repAvgDeal < avgDealSize * 0.8) { recommendedCoaching.push('Target larger opportunities'); } if (data.deals.length < wonDeals.length * 0.1) { recommendedCoaching.push('Increase activity and pipeline'); } repVelocities.push({ rep_name: repName, deals_closed: data.deals.length, total_revenue: data.revenue, avg_deal_size: repAvgDeal, avg_sales_cycle_days: repAvgCycle, win_rate: winRate, // Simplified: use overall win rate velocity_score: repVelocityScore, performance_rating: performanceRating, velocity_trend: 'Stable', recommended_coaching: recommendedCoaching, }); } repVelocities.sort((a, b) => b.velocity_score - a.velocity_score); } // Velocity trends const velocityTrends = []; const monthlyData = new Map(); for (const job of wonDeals) { const closedDate = job.date_status_change || job.date_updated || 0; if (closedDate === 0) continue; const monthKey = new Date(closedDate).toISOString().slice(0, 7); if (!monthlyData.has(monthKey)) { monthlyData.set(monthKey, { deals: 0, revenue: 0, cycleTimes: [] }); } const monthData = monthlyData.get(monthKey); monthData.deals++; monthData.revenue += parseFloat(job.total || job.value || 0); const created = job.date_created || 0; if (created > 0) { const cycleDays = (closedDate - created) / (24 * 60 * 60 * 1000); monthData.cycleTimes.push(cycleDays); } } const sortedMonths = Array.from(monthlyData.keys()).sort(); for (let i = 0; i < sortedMonths.length; i++) { const month = sortedMonths[i]; const data = monthlyData.get(month); const avgCycle = data.cycleTimes.length > 0 ? data.cycleTimes.reduce((sum, d) => sum + d, 0) / data.cycleTimes.length : avgSalesCycle; const velocity = data.revenue / Math.max(avgCycle, 1); const prevVelocity = i > 0 ? (monthlyData.get(sortedMonths[i - 1])?.revenue || 0) / avgSalesCycle : velocity; const momChange = prevVelocity > 0 ? ((velocity - prevVelocity) / prevVelocity) * 100 : 0; velocityTrends.push({ period: month, deals_closed: data.deals, revenue_generated: data.revenue, avg_cycle_time: avgCycle, velocity, trend_direction: momChange > 5 ? 'Up' : momChange < -5 ? 'Down' : 'Stable', month_over_month_change: momChange, }); } // Deal size segmentation const dealSegmentations = []; if (includeDealSegmentation) { const segments = [ { name: 'Enterprise', min: 50000, max: Infinity }, { name: 'Mid-Market', min: 10000, max: 49999 }, { name: 'Small Business', min: 2000, max: 9999 }, { name: 'Micro', min: 0, max: 1999 }, ]; for (const seg of segments) { const segDeals = wonDeals.filter((j) => { const value = parseFloat(j.total || j.value || 0); return value >= seg.min && value <= seg.max; }); const segRevenue = segDeals.reduce((sum, j) => sum + parseFloat(j.total || j.value || 0), 0); const segCycleTimes = segDeals .map((j) => { const created = j.date_created || 0; const closed = j.date_status_change || j.date_updated || 0; return created > 0 && closed > 0 ? (closed - created) / (24 * 60 * 60 * 1000) : 0; }) .filter((d) => d > 0); const segAvgCycle = segCycleTimes.length > 0 ? segCycleTimes.reduce((sum, d) => sum + d, 0) / segCycleTimes.length : avgSalesCycle; const velocityContribution = totalRevenueWon > 0 ? (segRevenue / totalRevenueWon) * 100 : 0; const optimizationRecs = []; if (segAvgCycle > avgSalesCycle * 1.2) { optimizationRecs.push('Streamline approval process for this segment'); } if (segDeals.length < wonDeals.length * 0.1 && seg.name !== 'Micro') { optimizationRecs.push(`Increase ${seg.name} targeting`); } dealSegmentations.push({ segment: seg.name, min_value: seg.min, max_value: seg.max === Infinity ? 999999999 : seg.max, deal_count: segDeals.length, total_value: segRevenue, avg_cycle_time: segAvgCycle, win_rate: winRate, // Simplified velocity_contribution: velocityContribution, optimization_recommendations: optimizationRecs, }); } } // Velocity optimization const velocityOptimizations = [ { optimization_area: 'Lead Response Time', current_state: 'Manual lead assignment, 24+ hour response', target_state: 'Automated routing, <1 hour response', expected_velocity_increase: 15, implementation_effort: 'Medium', roi_score: 85, action_plan: [ 'Implement round-robin lead assignment', 'Add auto-response email templates', 'Set up mobile notifications for reps', ], priority: 1, }, { optimization_area: 'Proposal Generation', current_state: 'Custom proposals from scratch', target_state: 'Template library with dynamic pricing', expected_velocity_increase: 20, implementation_effort: 'Low', roi_score: 90, action_plan: [ 'Create 5 proposal templates', 'Add pricing calculator', 'Enable e-signature integration', ], priority: 2, }, ]; return { data_source: 'Live JobNimbus API data', analysis_timestamp: new Date().toISOString(), time_window_days: timeWindowDays, sales_velocity_metrics: salesVelocityMetrics, sales_cycle_analysis: salesCycleAnalyses.slice(0, 10), win_loss_analysis: winLossAnalysis, pipeline_acceleration_opportunities: pipelineAccelerations, sales_rep_velocity: includeRepAnalysis ? repVelocities.slice(0, 10) : undefined, velocity_trends: velocityTrends.slice(-12), deal_size_segmentation: includeDealSegmentation ? dealSegmentations : undefined, velocity_optimization_recommendations: velocityOptimizations, key_insights: [ `Sales velocity: $${overallVelocity.toLocaleString()}/day`, `Average sales cycle: ${avgSalesCycle.toFixed(0)} days`, `Win rate: ${winRate.toFixed(1)}%`, `Velocity trend: ${velocityTrend}`, ], }; } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error', status: 'Failed', }; } } } //# sourceMappingURL=getSalesVelocityAnalytics.js.map