UNPKG

jobnimbus-mcp-client

Version:

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

271 lines 13.2 kB
/** * Get Job Summary * Comprehensive job analytics dashboard with KPIs, performance metrics, and trend analysis */ import { BaseTool } from '../baseTool.js'; export class GetJobSummaryTool extends BaseTool { get definition() { return { name: 'get_job_summary', description: 'Comprehensive job analytics dashboard with KPIs, performance metrics, status breakdowns, and trend analysis', inputSchema: { type: 'object', properties: { time_period_days: { type: 'number', default: 90, description: 'Time period for analysis in days (default: 90)', }, include_trends: { type: 'boolean', default: true, description: 'Include trend analysis', }, group_by_type: { type: 'boolean', default: true, description: 'Group metrics by job type', }, }, }, }; } async execute(input, context) { try { const timePeriodDays = input.time_period_days || 90; const includeTrends = input.include_trends !== false; const groupByType = input.group_by_type !== false; // Fetch data const [jobsResponse, estimatesResponse] = await Promise.all([ this.client.get(context.apiKey, 'jobs', { size: 100 }), this.client.get(context.apiKey, 'estimates', { size: 100 }), ]); const jobs = jobsResponse.data?.results || []; const estimates = estimatesResponse.data?.results || []; // Build estimate lookup const estimatesByJob = new Map(); for (const estimate of estimates) { const related = estimate.related || []; for (const rel of related) { if (rel.type === 'job' && rel.id) { if (!estimatesByJob.has(rel.id)) { estimatesByJob.set(rel.id, []); } estimatesByJob.get(rel.id).push(estimate); } } } const now = Date.now(); const cutoffDate = now - (timePeriodDays * 24 * 60 * 60 * 1000); // Filter jobs by time period const filteredJobs = jobs.filter((j) => { const createdDate = j.date_created || 0; return createdDate >= cutoffDate; }); // Calculate KPIs let totalJobs = 0; let activeJobs = 0; let completedJobs = 0; let cancelledJobs = 0; let totalRevenue = 0; let pendingRevenue = 0; // Status breakdown const statusMap = new Map(); // Job type metrics const jobTypeMap = new Map(); for (const job of filteredJobs) { totalJobs++; const statusName = job.status_name || 'Unknown'; const jobType = job.job_type_name || 'Unspecified'; const isComplete = statusName.toLowerCase().includes('complete') || statusName.toLowerCase().includes('won') || statusName.toLowerCase().includes('sold'); const isCancelled = statusName.toLowerCase().includes('cancel') || statusName.toLowerCase().includes('lost'); // Count by status if (isComplete) { completedJobs++; } else if (isCancelled) { cancelledJobs++; } else { activeJobs++; } // Calculate job value let jobValue = 0; const jobEstimates = estimatesByJob.get(job.jnid) || []; for (const est of jobEstimates) { const estValue = parseFloat(est.total || 0); jobValue += estValue; if (est.date_signed > 0 || est.status_name === 'approved') { totalRevenue += estValue; } else { pendingRevenue += estValue; } } // Status breakdown if (!statusMap.has(statusName)) { statusMap.set(statusName, { count: 0, totalValue: 0, totalAge: 0 }); } const statusData = statusMap.get(statusName); statusData.count++; statusData.totalValue += jobValue; const createdDate = job.date_created || now; const ageInDays = (now - createdDate) / (24 * 60 * 60 * 1000); statusData.totalAge += ageInDays; // Job type metrics if (groupByType) { if (!jobTypeMap.has(jobType)) { jobTypeMap.set(jobType, { count: 0, revenue: 0, completed: 0, totalCycleTime: 0, }); } const typeData = jobTypeMap.get(jobType); typeData.count++; typeData.revenue += jobValue; if (isComplete) { typeData.completed++; const cycleTime = job.date_updated && job.date_created ? (job.date_updated - job.date_created) / (24 * 60 * 60 * 1000) : 0; typeData.totalCycleTime += cycleTime; } } } // Build KPI summary const avgJobValue = totalJobs > 0 ? (totalRevenue + pendingRevenue) / totalJobs : 0; const completionRate = totalJobs > 0 ? (completedJobs / totalJobs) * 100 : 0; const kpis = { total_jobs: totalJobs, active_jobs: activeJobs, completed_jobs: completedJobs, cancelled_jobs: cancelledJobs, total_revenue: totalRevenue, pending_revenue: pendingRevenue, avg_job_value: avgJobValue, completion_rate: completionRate, }; // Status breakdown const statusBreakdown = Array.from(statusMap.entries()) .map(([statusName, data]) => ({ status_name: statusName, count: data.count, percentage: (data.count / totalJobs) * 100, total_value: data.totalValue, avg_value: data.count > 0 ? data.totalValue / data.count : 0, avg_age_days: data.count > 0 ? data.totalAge / data.count : 0, })) .sort((a, b) => b.count - a.count); // Job type metrics const jobTypeMetrics = Array.from(jobTypeMap.entries()) .map(([jobType, data]) => ({ job_type: jobType, count: data.count, revenue: data.revenue, avg_value: data.count > 0 ? data.revenue / data.count : 0, completion_rate: data.count > 0 ? (data.completed / data.count) * 100 : 0, avg_cycle_time_days: data.completed > 0 ? data.totalCycleTime / data.completed : 0, })) .sort((a, b) => b.revenue - a.revenue); // Trend analysis let trends = null; if (includeTrends && filteredJobs.length >= 10) { // Split into two halves const midpoint = Math.floor(filteredJobs.length / 2); const firstHalf = filteredJobs.slice(0, midpoint); const secondHalf = filteredJobs.slice(midpoint); const firstHalfRevenue = firstHalf.reduce((sum, j) => { const jobEsts = estimatesByJob.get(j.jnid) || []; return sum + jobEsts.reduce((s, e) => s + parseFloat(e.total || 0), 0); }, 0); const secondHalfRevenue = secondHalf.reduce((sum, j) => { const jobEsts = estimatesByJob.get(j.jnid) || []; return sum + jobEsts.reduce((s, e) => s + parseFloat(e.total || 0), 0); }, 0); const revenueGrowth = firstHalfRevenue > 0 ? ((secondHalfRevenue - firstHalfRevenue) / firstHalfRevenue) * 100 : 0; const firstHalfCompleted = firstHalf.filter((j) => { const status = (j.status_name || '').toLowerCase(); return status.includes('complete') || status.includes('won'); }).length; const secondHalfCompleted = secondHalf.filter((j) => { const status = (j.status_name || '').toLowerCase(); return status.includes('complete') || status.includes('won'); }).length; const firstHalfCompletionRate = firstHalf.length > 0 ? (firstHalfCompleted / firstHalf.length) * 100 : 0; const secondHalfCompletionRate = secondHalf.length > 0 ? (secondHalfCompleted / secondHalf.length) * 100 : 0; trends = { revenue_trend: revenueGrowth > 5 ? 'Growing' : revenueGrowth < -5 ? 'Declining' : 'Stable', revenue_growth_rate: revenueGrowth, completion_rate_trend: secondHalfCompletionRate > firstHalfCompletionRate ? 'Improving' : secondHalfCompletionRate < firstHalfCompletionRate ? 'Declining' : 'Stable', volume_trend: secondHalf.length > firstHalf.length ? 'Increasing' : secondHalf.length < firstHalf.length ? 'Decreasing' : 'Stable', }; } // Key insights const insights = []; if (completionRate < 50) { insights.push(`⚠️ LOW completion rate at ${completionRate.toFixed(1)}% - review sales process`); } else if (completionRate > 75) { insights.push(`✅ EXCELLENT completion rate at ${completionRate.toFixed(1)}%`); } if (pendingRevenue > totalRevenue) { insights.push(`💰 Large pipeline: $${pendingRevenue.toFixed(2)} pending vs $${totalRevenue.toFixed(2)} closed`); } const avgCycleTime = jobTypeMetrics.length > 0 ? jobTypeMetrics.reduce((sum, jt) => sum + jt.avg_cycle_time_days, 0) / jobTypeMetrics.length : 0; if (avgCycleTime > 30) { insights.push(`⏱️ Long sales cycle: ${avgCycleTime.toFixed(1)} days average - consider acceleration strategies`); } const topJobType = jobTypeMetrics.length > 0 ? jobTypeMetrics[0] : null; if (topJobType) { insights.push(`🏆 Top performer: ${topJobType.job_type} with $${topJobType.revenue.toFixed(2)} revenue`); } if (trends && trends.revenue_trend === 'Declining') { insights.push(`📉 Revenue declining at ${trends.revenue_growth_rate.toFixed(1)}% - immediate action needed`); } else if (trends && trends.revenue_trend === 'Growing') { insights.push(`📈 Revenue growing at ${trends.revenue_growth_rate.toFixed(1)}% - continue momentum`); } return { data_source: 'Live JobNimbus API data', analysis_timestamp: new Date().toISOString(), time_period: { days: timePeriodDays, start_date: new Date(cutoffDate).toISOString(), end_date: new Date(now).toISOString(), }, kpis: kpis, status_breakdown: statusBreakdown, job_type_metrics: groupByType ? jobTypeMetrics : undefined, trends: trends, key_insights: insights, performance_rating: { completion_rate: completionRate >= 75 ? 'Excellent' : completionRate >= 50 ? 'Good' : completionRate >= 25 ? 'Fair' : 'Poor', revenue_health: pendingRevenue > totalRevenue * 0.5 ? 'Healthy Pipeline' : 'Pipeline Needs Attention', overall: completionRate >= 60 && pendingRevenue > totalRevenue * 0.3 ? 'Strong' : 'Needs Improvement', }, }; } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error', status: 'Failed', }; } } } //# sourceMappingURL=getJobSummary.js.map