jobnimbus-mcp-client
Version:
JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server
391 lines • 19.5 kB
JavaScript
/**
* Get Communication Analytics
* Comprehensive communication tracking with call/email/text analysis, response times, engagement metrics, and outreach effectiveness
*/
import { BaseTool } from '../baseTool.js';
export class GetCommunicationAnalyticsTool extends BaseTool {
get definition() {
return {
name: 'get_communication_analytics',
description: 'Comprehensive communication tracking with call/email/text analysis, response times, engagement metrics, time-based patterns, and outreach effectiveness scoring',
inputSchema: {
type: 'object',
properties: {
user_filter: {
type: 'string',
description: 'Filter by specific user name or ID',
},
communication_type: {
type: 'string',
enum: ['call', 'email', 'text', 'meeting', 'all'],
default: 'all',
description: 'Filter by communication type',
},
include_time_analysis: {
type: 'boolean',
default: true,
description: 'Include time-of-day effectiveness analysis',
},
include_user_stats: {
type: 'boolean',
default: true,
description: 'Include per-user communication statistics',
},
days_back: {
type: 'number',
default: 30,
description: 'Days of history to analyze (default: 30)',
},
},
},
};
}
async execute(input, context) {
try {
const userFilter = input.user_filter;
const commType = input.communication_type || 'all';
const includeTimeAnalysis = input.include_time_analysis !== false;
const includeUserStats = input.include_user_stats !== false;
const daysBack = input.days_back || 30;
// Fetch data
const [activitiesResponse, jobsResponse] = await Promise.all([
this.client.get(context.apiKey, 'activities', { size: 100 }),
this.client.get(context.apiKey, 'jobs', { size: 100 }),
]);
const activities = activitiesResponse.data?.activity || [];
const jobs = jobsResponse.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 || usersResponse.data?.users || [];
}
catch (error) {
// Users endpoint not available - proceed without user attribution
console.warn('Users endpoint not available - communication analytics will be limited');
}
const now = Date.now();
const cutoffDate = now - (daysBack * 24 * 60 * 60 * 1000);
// Build user lookup
const userLookup = new Map();
for (const user of users) {
if (user.jnid || user.id) {
userLookup.set(user.jnid || user.id, user);
}
}
// Build contact and job conversion maps
const contactConversions = new Set();
const jobConversions = new Set();
for (const job of jobs) {
const statusLower = (job.status_name || '').toLowerCase();
if (statusLower.includes('complete') || statusLower.includes('won')) {
const related = job.related || [];
for (const rel of related) {
if (rel.type === 'contact' && rel.id) {
contactConversions.add(rel.id);
}
}
if (job.jnid)
jobConversions.add(job.jnid);
}
}
// Filter communication activities
const communications = activities.filter((act) => {
const createdDate = act.date_created || act.created_at || 0;
if (createdDate < cutoffDate)
return false;
const activityType = (act.activity_type || act.type || '').toLowerCase();
if (commType !== 'all') {
return activityType.includes(commType.toLowerCase());
}
return activityType.includes('call') ||
activityType.includes('email') ||
activityType.includes('text') ||
activityType.includes('sms') ||
activityType.includes('message') ||
activityType.includes('meeting');
});
// Overall metrics
const metrics = {
total_communications: communications.length,
calls: 0,
emails: 0,
texts: 0,
meetings: 0,
avg_response_time_hours: 0,
communication_rate_per_day: communications.length / daysBack,
active_communication_threads: 0,
};
const responseTimes = [];
const typeMap = new Map();
const hourlyMap = new Map();
const userStatsMap = new Map();
const channelEffectiveness = new Map();
// Process communications
for (const comm of communications) {
const activityType = (comm.activity_type || comm.type || '').toLowerCase();
const createdDate = comm.date_created || comm.created_at || 0;
// Count by type
if (activityType.includes('call'))
metrics.calls++;
else if (activityType.includes('email'))
metrics.emails++;
else if (activityType.includes('text') || activityType.includes('sms'))
metrics.texts++;
else if (activityType.includes('meeting'))
metrics.meetings++;
// Type distribution
const simplifiedType = this.simplifyType(activityType);
if (!typeMap.has(simplifiedType)) {
typeMap.set(simplifiedType, { count: 0, durations: [], success: 0, followUps: 0 });
}
const typeData = typeMap.get(simplifiedType);
typeData.count++;
// Duration tracking
const duration = comm.duration || comm.duration_minutes || 0;
if (duration > 0)
typeData.durations.push(duration);
// Success tracking (if completed or has outcome)
const statusLower = (comm.status_name || comm.status || '').toLowerCase();
if (statusLower.includes('complete') || statusLower.includes('success')) {
typeData.success++;
}
// Follow-up tracking
if (comm.follow_up || comm.requires_follow_up) {
typeData.followUps++;
}
// Response time
const completedDate = comm.date_completed || comm.date_updated || 0;
if (completedDate > 0 && createdDate > 0) {
const responseTime = (completedDate - createdDate) / (1000 * 60 * 60); // hours
responseTimes.push(responseTime);
}
// Hourly analysis
if (includeTimeAnalysis && createdDate > 0) {
const hour = new Date(createdDate).getHours();
if (!hourlyMap.has(hour)) {
hourlyMap.set(hour, { count: 0, success: 0 });
}
const hourData = hourlyMap.get(hour);
hourData.count++;
if (statusLower.includes('complete') || statusLower.includes('success')) {
hourData.success++;
}
}
// User stats
if (includeUserStats) {
const userId = comm.created_by || comm.user_id || 'unknown';
const user = userLookup.get(userId);
// Apply user filter
if (userFilter) {
const userName = user?.display_name || user?.name || '';
if (!userName.toLowerCase().includes(userFilter.toLowerCase()) && userId !== userFilter) {
continue;
}
}
if (!userStatsMap.has(userId)) {
userStatsMap.set(userId, {
user: user || { display_name: 'Unknown', email: userId },
outreach: 0,
calls: 0,
emails: 0,
texts: 0,
responseTimes: [],
conversions: 0,
});
}
const userStats = userStatsMap.get(userId);
userStats.outreach++;
if (activityType.includes('call'))
userStats.calls++;
else if (activityType.includes('email'))
userStats.emails++;
else if (activityType.includes('text') || activityType.includes('sms'))
userStats.texts++;
if (completedDate > 0 && createdDate > 0) {
userStats.responseTimes.push((completedDate - createdDate) / (1000 * 60 * 60));
}
// Check for conversions
const related = comm.related || [];
for (const rel of related) {
if (rel.type === 'contact' && rel.id && contactConversions.has(rel.id)) {
userStats.conversions++;
}
if (rel.type === 'job' && rel.id && jobConversions.has(rel.id)) {
userStats.conversions++;
}
}
}
// Channel effectiveness
if (!channelEffectiveness.has(simplifiedType)) {
channelEffectiveness.set(simplifiedType, { outreach: 0, responses: 0, conversions: 0 });
}
const channelData = channelEffectiveness.get(simplifiedType);
channelData.outreach++;
if (statusLower.includes('complete') || statusLower.includes('response')) {
channelData.responses++;
}
const related = comm.related || [];
for (const rel of related) {
if ((rel.type === 'contact' && rel.id && contactConversions.has(rel.id)) ||
(rel.type === 'job' && rel.id && jobConversions.has(rel.id))) {
channelData.conversions++;
break;
}
}
}
// Calculate metrics
metrics.avg_response_time_hours = responseTimes.length > 0
? responseTimes.reduce((sum, t) => sum + t, 0) / responseTimes.length
: 0;
// Communication type distribution
const communicationTypes = [];
for (const [type, data] of typeMap.entries()) {
communicationTypes.push({
type: type,
count: data.count,
percentage: (data.count / communications.length) * 100,
avg_duration_minutes: data.durations.length > 0
? data.durations.reduce((sum, d) => sum + d, 0) / data.durations.length
: 0,
success_rate: data.count > 0 ? (data.success / data.count) * 100 : 0,
follow_up_rate: data.count > 0 ? (data.followUps / data.count) * 100 : 0,
});
}
communicationTypes.sort((a, b) => b.count - a.count);
// User communication stats
const userCommunicationStats = [];
for (const [userId, stats] of userStatsMap.entries()) {
const avgResponseTime = stats.responseTimes.length > 0
? stats.responseTimes.reduce((sum, t) => sum + t, 0) / stats.responseTimes.length
: 0;
const engagementScore = this.calculateEngagementScore(stats.outreach, stats.calls, stats.emails, avgResponseTime);
const conversionRate = stats.outreach > 0 ? (stats.conversions / stats.outreach) * 100 : 0;
userCommunicationStats.push({
user_id: userId,
user_name: stats.user.display_name || stats.user.name || 'Unknown',
total_outreach: stats.outreach,
calls_made: stats.calls,
emails_sent: stats.emails,
texts_sent: stats.texts,
avg_response_time_hours: avgResponseTime,
engagement_score: engagementScore,
conversion_rate: conversionRate,
top_performer: false, // Will be set later
});
}
// Sort by engagement score and mark top performers
userCommunicationStats.sort((a, b) => b.engagement_score - a.engagement_score);
if (userCommunicationStats.length > 0) {
userCommunicationStats[0].top_performer = true;
}
// Time-based analysis
const timeBasedAnalysis = [];
if (includeTimeAnalysis) {
for (let hour = 0; hour < 24; hour++) {
const hourData = hourlyMap.get(hour) || { count: 0, success: 0 };
const successRate = hourData.count > 0 ? (hourData.success / hourData.count) * 100 : 0;
const effectivenessScore = this.calculateEffectivenessScore(hourData.count, successRate);
timeBasedAnalysis.push({
hour_of_day: hour,
communication_count: hourData.count,
success_rate: successRate,
effectiveness_score: effectivenessScore,
});
}
}
// Outreach effectiveness
const outreachEffectiveness = [];
for (const [channel, data] of channelEffectiveness.entries()) {
const responseRate = data.outreach > 0 ? (data.responses / data.outreach) * 100 : 0;
const conversionRate = data.outreach > 0 ? (data.conversions / data.outreach) * 100 : 0;
const roiScore = this.calculateROIScore(responseRate, conversionRate);
outreachEffectiveness.push({
channel: channel,
total_outreach: data.outreach,
responses_received: data.responses,
response_rate: responseRate,
leads_converted: data.conversions,
conversion_rate: conversionRate,
roi_score: roiScore,
});
}
outreachEffectiveness.sort((a, b) => b.roi_score - a.roi_score);
// Recommendations
const recommendations = [];
if (metrics.avg_response_time_hours > 24) {
recommendations.push(`⏱️ High average response time (${metrics.avg_response_time_hours.toFixed(1)}h) - improve responsiveness`);
}
const topChannel = outreachEffectiveness[0];
if (topChannel && topChannel.roi_score >= 70) {
recommendations.push(`🏆 ${topChannel.channel} is most effective channel (${topChannel.roi_score}/100 ROI score)`);
}
const topPerformer = userCommunicationStats[0];
if (topPerformer) {
recommendations.push(`👤 Top communicator: ${topPerformer.user_name} (${topPerformer.engagement_score}/100 engagement)`);
}
const bestHours = timeBasedAnalysis
.sort((a, b) => b.effectiveness_score - a.effectiveness_score)
.slice(0, 3)
.map(t => `${t.hour_of_day}:00`);
if (bestHours.length > 0) {
recommendations.push(`🕐 Most effective hours: ${bestHours.join(', ')}`);
}
return {
data_source: 'Live JobNimbus API data',
analysis_timestamp: new Date().toISOString(),
analysis_period_days: daysBack,
summary: metrics,
communication_types: communicationTypes,
user_communication_stats: includeUserStats ? userCommunicationStats : undefined,
time_based_analysis: includeTimeAnalysis ? timeBasedAnalysis : undefined,
outreach_effectiveness: outreachEffectiveness,
recommendations: recommendations,
key_insights: [
`${metrics.total_communications} communications tracked`,
`Average response time: ${metrics.avg_response_time_hours.toFixed(1)} hours`,
`Most used channel: ${communicationTypes[0]?.type || 'N/A'}`,
`${Math.round(metrics.communication_rate_per_day)} communications per day`,
],
};
}
catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 'Failed',
};
}
}
simplifyType(type) {
if (type.includes('call'))
return 'Call';
if (type.includes('email'))
return 'Email';
if (type.includes('text') || type.includes('sms'))
return 'Text/SMS';
if (type.includes('meeting'))
return 'Meeting';
return 'Other';
}
calculateEngagementScore(outreach, calls, emails, avgResponseTime) {
let score = 0;
// Volume (40 points)
score += Math.min((outreach / 50) * 40, 40);
// Diversity (30 points)
const diversity = [calls > 0, emails > 0].filter(Boolean).length;
score += (diversity / 2) * 30;
// Responsiveness (30 points)
const responsivenessScore = avgResponseTime > 0 ? Math.max(0, 30 - (avgResponseTime / 2)) : 15;
score += Math.min(responsivenessScore, 30);
return Math.min(Math.round(score), 100);
}
calculateEffectivenessScore(count, successRate) {
const volumeScore = Math.min((count / 10) * 50, 50);
const qualityScore = (successRate / 100) * 50;
return Math.round(volumeScore + qualityScore);
}
calculateROIScore(responseRate, conversionRate) {
return Math.round((responseRate * 0.4) + (conversionRate * 0.6));
}
}
//# sourceMappingURL=getCommunicationAnalytics.js.map