jobnimbus-mcp-client
Version:
JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server
252 lines • 11.8 kB
JavaScript
/**
* Get Revenue Report - Comprehensive revenue reporting and analysis
* Provides detailed revenue breakdown by period, job type, and sales rep
*/
import { BaseTool } from '../baseTool.js';
export class GetRevenueReportTool extends BaseTool {
get definition() {
return {
name: 'get_revenue_report',
description: 'Comprehensive revenue reporting and analysis',
inputSchema: {
type: 'object',
properties: {
period: {
type: 'string',
enum: ['current_month', 'last_month', 'current_quarter', 'last_quarter', 'ytd', 'all_time'],
default: 'current_month',
description: 'Time period for analysis',
},
include_pending: {
type: 'boolean',
default: false,
description: 'Include pending estimates in projections',
},
},
},
};
}
async execute(input, context) {
try {
const period = input.period || 'current_month';
const includePending = input.include_pending || false;
// Fetch comprehensive 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 by job
const estimatesByJob = new Map();
for (const estimate of estimates) {
const related = estimate.related || [];
for (const rel of related) {
if (rel.type === 'job' && rel.id) {
const jobId = rel.id;
if (!estimatesByJob.has(jobId)) {
estimatesByJob.set(jobId, []);
}
estimatesByJob.get(jobId).push(estimate);
}
}
}
// Calculate period boundaries
const now = new Date();
const periodStart = this.getPeriodStart(period, now);
// Analyze revenue
let totalRevenue = 0;
let approvedRevenue = 0;
let pendingRevenue = 0;
let approvedCount = 0;
let pendingCount = 0;
const revenueByType = new Map();
const revenueByRep = new Map();
const monthlyRevenue = new Map();
for (const job of jobs) {
if (!job.jnid)
continue;
const jobDate = job.date_created || 0;
if (periodStart && jobDate < periodStart.getTime())
continue;
const jobEstimates = estimatesByJob.get(job.jnid) || [];
let jobRevenue = 0;
let jobApproved = false;
for (const estimate of jobEstimates) {
const estimateValue = parseFloat(estimate.total || 0) || 0;
const statusName = (estimate.status_name || '').toLowerCase();
const isSigned = estimate.date_signed > 0;
const isApproved = isSigned || statusName === 'approved' || statusName === 'signed';
if (isApproved) {
jobRevenue += estimateValue;
approvedRevenue += estimateValue;
jobApproved = true;
}
else if (includePending) {
pendingRevenue += estimateValue;
pendingCount += 1;
}
}
if (jobRevenue > 0 || (includePending && jobEstimates.length > 0)) {
totalRevenue += jobRevenue;
if (jobApproved)
approvedCount += 1;
// By job type
const jobType = job.record_type_name || 'Unknown';
if (!revenueByType.has(jobType)) {
revenueByType.set(jobType, { revenue: 0, count: 0 });
}
const typeStats = revenueByType.get(jobType);
typeStats.revenue += jobRevenue;
typeStats.count += 1;
// By sales rep
const repId = job.sales_rep || job.assigned_to || job.created_by || 'Unknown';
const repName = job.sales_rep_name || 'Unknown';
if (!revenueByRep.has(repId)) {
revenueByRep.set(repId, { revenue: 0, count: 0, name: repName });
}
const repStats = revenueByRep.get(repId);
repStats.revenue += jobRevenue;
repStats.count += 1;
// By month
const monthKey = new Date(jobDate).toISOString().substring(0, 7); // YYYY-MM
if (!monthlyRevenue.has(monthKey)) {
monthlyRevenue.set(monthKey, { revenue: 0, count: 0 });
}
const monthStats = monthlyRevenue.get(monthKey);
monthStats.revenue += jobRevenue;
monthStats.count += 1;
}
}
// Build revenue by type array
const revenueByTypeArray = Array.from(revenueByType.entries())
.map(([type, stats]) => ({
job_type: type,
total_revenue: stats.revenue,
job_count: stats.count,
avg_revenue: stats.count > 0 ? stats.revenue / stats.count : 0,
percentage_of_total: totalRevenue > 0 ? (stats.revenue / totalRevenue) * 100 : 0,
}))
.sort((a, b) => b.total_revenue - a.total_revenue);
// Build revenue by rep array
const revenueByRepArray = Array.from(revenueByRep.entries())
.map(([repId, stats]) => ({
rep_id: repId,
rep_name: stats.name,
total_revenue: stats.revenue,
job_count: stats.count,
avg_deal_size: stats.count > 0 ? stats.revenue / stats.count : 0,
percentage_of_total: totalRevenue > 0 ? (stats.revenue / totalRevenue) * 100 : 0,
}))
.sort((a, b) => b.total_revenue - a.total_revenue);
// Build monthly trend
const monthlyTrend = Array.from(monthlyRevenue.entries())
.map(([month, stats]) => ({
period: month,
total_revenue: stats.revenue,
job_count: stats.count,
avg_revenue: stats.count > 0 ? stats.revenue / stats.count : 0,
approved_estimates: stats.count,
pending_estimates: 0,
}))
.sort((a, b) => a.period.localeCompare(b.period));
return {
data_source: 'Live JobNimbus API data',
analysis_timestamp: new Date().toISOString(),
period: {
selected: period,
start_date: periodStart?.toISOString() || null,
end_date: now.toISOString(),
},
summary: {
total_revenue: totalRevenue,
approved_revenue: approvedRevenue,
pending_revenue: pendingRevenue,
total_jobs: approvedCount,
pending_estimates: pendingCount,
average_deal_size: approvedCount > 0 ? totalRevenue / approvedCount : 0,
projected_total: includePending ? totalRevenue + pendingRevenue : totalRevenue,
},
revenue_by_job_type: revenueByTypeArray,
revenue_by_sales_rep: revenueByRepArray.slice(0, 10),
monthly_trend: monthlyTrend,
insights: this.generateInsights(totalRevenue, approvedCount, revenueByTypeArray, revenueByRepArray, monthlyTrend),
recommendations: this.generateRecommendations(totalRevenue, pendingRevenue, approvedCount, pendingCount),
};
}
catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 'Failed',
};
}
}
getPeriodStart(period, now) {
const year = now.getFullYear();
const month = now.getMonth();
const quarter = Math.floor(month / 3);
switch (period) {
case 'current_month':
return new Date(year, month, 1);
case 'last_month':
return new Date(year, month - 1, 1);
case 'current_quarter':
return new Date(year, quarter * 3, 1);
case 'last_quarter':
return new Date(year, (quarter - 1) * 3, 1);
case 'ytd':
return new Date(year, 0, 1);
case 'all_time':
return null;
default:
return new Date(year, month, 1);
}
}
generateInsights(totalRevenue, jobCount, byType, byRep, trend) {
const insights = [];
// Revenue concentration
if (byType.length > 0) {
const topType = byType[0];
insights.push(`${topType.job_type} is the top revenue generator (${topType.percentage_of_total.toFixed(1)}% of total)`);
}
// Top performer
if (byRep.length > 0) {
const topRep = byRep[0];
insights.push(`${topRep.rep_name} leads in revenue with $${topRep.total_revenue.toFixed(2)} (${topRep.percentage_of_total.toFixed(1)}%)`);
}
// Trend analysis
if (trend.length >= 2) {
const current = trend[trend.length - 1];
const previous = trend[trend.length - 2];
const growth = ((current.total_revenue - previous.total_revenue) / previous.total_revenue) * 100;
if (Math.abs(growth) > 5) {
insights.push(`Revenue ${growth > 0 ? 'increased' : 'decreased'} by ${Math.abs(growth).toFixed(1)}% vs previous period`);
}
}
// Average deal size
const avgDeal = jobCount > 0 ? totalRevenue / jobCount : 0;
insights.push(`Average deal size: $${avgDeal.toFixed(2)} across ${jobCount} jobs`);
return insights;
}
generateRecommendations(approvedRevenue, pendingRevenue, approvedCount, pendingCount) {
const recommendations = [];
// Pending opportunities
if (pendingRevenue > approvedRevenue * 0.2) {
recommendations.push(`Focus on converting ${pendingCount} pending estimates worth $${pendingRevenue.toFixed(2)}`);
}
// Low volume warning
if (approvedCount < 10) {
recommendations.push('Low job volume - increase lead generation and sales activities');
}
// High pending ratio
const totalEstimates = approvedCount + pendingCount;
if (totalEstimates > 0 && pendingCount / totalEstimates > 0.5) {
recommendations.push('High pending estimate ratio - review pricing and follow-up processes');
}
if (recommendations.length === 0) {
recommendations.push('Revenue performance is healthy - maintain current sales strategies');
}
return recommendations;
}
}
//# sourceMappingURL=getRevenueReport.js.map