jobnimbus-mcp-client
Version:
JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server
393 lines • 21.4 kB
JavaScript
/**
* Get User Productivity Analytics
* Comprehensive team and individual productivity analysis with activity metrics, performance scoring, collaboration patterns, and workload optimization
*/
import { BaseTool } from '../baseTool.js';
export class GetUserProductivityAnalyticsTool extends BaseTool {
get definition() {
return {
name: 'get_user_productivity_analytics',
description: 'Comprehensive team and individual productivity analysis with activity metrics, performance scoring, collaboration patterns, and workload optimization recommendations',
inputSchema: {
type: 'object',
properties: {
user_filter: {
type: 'string',
description: 'Filter by specific user name or email',
},
include_activity_patterns: {
type: 'boolean',
default: true,
description: 'Include detailed activity pattern analysis',
},
include_collaboration_metrics: {
type: 'boolean',
default: true,
description: 'Include collaboration and team interaction analysis',
},
include_workload_analysis: {
type: 'boolean',
default: true,
description: 'Include workload distribution and capacity analysis',
},
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 includePatterns = input.include_activity_patterns !== false;
const includeCollaboration = input.include_collaboration_metrics !== false;
const includeWorkload = input.include_workload_analysis !== false;
const daysBack = input.days_back || 30;
// Fetch data
const [activitiesResponse, jobsResponse, contactsResponse, estimatesResponse] = await Promise.all([
this.client.get(context.apiKey, 'activities', { size: 100 }),
this.client.get(context.apiKey, 'jobs', { size: 100 }),
this.client.get(context.apiKey, 'contacts', { size: 100 }),
this.client.get(context.apiKey, 'estimates', { size: 100 }),
]);
const activities = activitiesResponse.data?.activity || [];
const jobs = jobsResponse.data?.results || [];
const contacts = contactsResponse.data?.results || [];
const estimates = estimatesResponse.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 - user productivity analysis will be limited');
}
const now = Date.now();
const cutoffDate = now - (daysBack * 24 * 60 * 60 * 1000);
// Build user metrics map
const userMetricsMap = new Map();
// Initialize user metrics
for (const user of users) {
const userId = user.jnid || user.id || user.email;
if (!userId)
continue;
// Apply user filter
if (userFilter) {
const userName = user.display_name || user.name || '';
const userEmail = user.email || '';
if (!userName.toLowerCase().includes(userFilter.toLowerCase()) &&
!userEmail.toLowerCase().includes(userFilter.toLowerCase())) {
continue;
}
}
userMetricsMap.set(userId, {
user: user,
activities: [],
jobsCreated: 0,
contactsCreated: 0,
estimatesCreated: 0,
tasksCompleted: 0,
responseTimes: [],
contactsEngaged: new Set(),
jobsCollaborated: new Set(),
activityTypes: new Map(),
hourlyActivity: new Map(),
dailyActivity: new Map(),
});
}
// Process activities
for (const activity of activities) {
const createdDate = activity.date_created || activity.created_at || 0;
if (createdDate < cutoffDate)
continue;
const userId = activity.created_by || activity.user_id || '';
if (!userId || !userMetricsMap.has(userId))
continue;
const metrics = userMetricsMap.get(userId);
metrics.activities.push(activity);
// Activity type distribution
const activityType = activity.activity_type || activity.type || 'General';
metrics.activityTypes.set(activityType, (metrics.activityTypes.get(activityType) || 0) + 1);
// Task completion
if (activityType.toLowerCase().includes('task')) {
const status = (activity.status_name || activity.status || '').toLowerCase();
if (status.includes('complete') || status.includes('done')) {
metrics.tasksCompleted++;
// Response time
const completedDate = activity.date_completed || activity.date_updated || 0;
if (completedDate > 0 && createdDate > 0) {
metrics.responseTimes.push((completedDate - createdDate) / (1000 * 60 * 60));
}
}
}
// Hourly pattern
const hour = new Date(createdDate).getHours();
metrics.hourlyActivity.set(hour, (metrics.hourlyActivity.get(hour) || 0) + 1);
// Daily pattern
const dayName = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][new Date(createdDate).getDay()];
metrics.dailyActivity.set(dayName, (metrics.dailyActivity.get(dayName) || 0) + 1);
// Collaboration tracking
const related = activity.related || [];
for (const rel of related) {
if (rel.type === 'contact' && rel.id) {
metrics.contactsEngaged.add(rel.id);
}
if (rel.type === 'job' && rel.id) {
metrics.jobsCollaborated.add(rel.id);
}
}
}
// Process jobs
for (const job of jobs) {
const createdBy = job.created_by || job.owner || '';
if (!createdBy || !userMetricsMap.has(createdBy))
continue;
const createdDate = job.date_created || 0;
if (createdDate >= cutoffDate) {
userMetricsMap.get(createdBy).jobsCreated++;
}
}
// Process contacts
for (const contact of contacts) {
const createdBy = contact.created_by || '';
if (!createdBy || !userMetricsMap.has(createdBy))
continue;
const createdDate = contact.date_created || 0;
if (createdDate >= cutoffDate) {
userMetricsMap.get(createdBy).contactsCreated++;
}
}
// Process estimates
for (const estimate of estimates) {
const createdBy = estimate.created_by || estimate.sales_rep || '';
if (!createdBy || !userMetricsMap.has(createdBy))
continue;
const createdDate = estimate.date_created || 0;
if (createdDate >= cutoffDate) {
userMetricsMap.get(createdBy).estimatesCreated++;
}
}
// Calculate user productivity metrics
const userProductivityMetrics = [];
const activityPatterns = [];
const collaborationMetrics = [];
const workloadDistribution = [];
for (const [userId, metrics] of userMetricsMap.entries()) {
const user = metrics.user;
const totalActivities = metrics.activities.length;
// Skip inactive users
if (totalActivities === 0)
continue;
const avgResponseTime = metrics.responseTimes.length > 0
? metrics.responseTimes.reduce((sum, t) => sum + t, 0) / metrics.responseTimes.length
: 0;
// Calculate productivity score
const productivityScore = this.calculateProductivityScore(metrics.jobsCreated, metrics.contactsCreated, metrics.estimatesCreated, metrics.tasksCompleted, totalActivities, avgResponseTime);
const efficiencyRating = productivityScore >= 80 ? 'Excellent' :
productivityScore >= 60 ? 'Good' :
productivityScore >= 40 ? 'Fair' : 'Needs Improvement';
// Workload balance (0-100, where 50 is perfectly balanced)
const workloadBalance = this.calculateWorkloadBalance(metrics.jobsCreated, totalActivities);
// Collaboration score
const collaborationScore = this.calculateCollaborationScore(metrics.contactsEngaged.size, metrics.jobsCollaborated.size, totalActivities);
userProductivityMetrics.push({
user_id: userId,
user_name: user.display_name || user.name || 'Unknown',
user_email: user.email || '',
role: user.role || user.job_title || 'Team Member',
total_activities: totalActivities,
jobs_created: metrics.jobsCreated,
contacts_created: metrics.contactsCreated,
estimates_created: metrics.estimatesCreated,
tasks_completed: metrics.tasksCompleted,
avg_response_time_hours: avgResponseTime,
productivity_score: productivityScore,
efficiency_rating: efficiencyRating,
workload_balance: workloadBalance,
collaboration_score: collaborationScore,
});
// Activity patterns
if (includePatterns) {
const peakHours = Array.from(metrics.hourlyActivity.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([hour]) => `${hour}:00-${hour + 1}:00`);
const mostCommonType = Array.from(metrics.activityTypes.entries())
.sort((a, b) => b[1] - a[1])[0]?.[0] || 'None';
const activityDistribution = {};
for (const [type, count] of metrics.activityTypes.entries()) {
activityDistribution[type] = count;
}
const weeklyPattern = Array.from(metrics.dailyActivity.entries())
.map(([day, count]) => ({ day, activity_count: count }));
activityPatterns.push({
user_id: userId,
peak_activity_hours: peakHours,
most_common_activity_type: mostCommonType,
activity_distribution: activityDistribution,
weekly_pattern: weeklyPattern,
});
}
// Collaboration metrics
if (includeCollaboration) {
collaborationMetrics.push({
user_id: userId,
unique_contacts_engaged: metrics.contactsEngaged.size,
jobs_collaborated: metrics.jobsCollaborated.size,
team_interactions: totalActivities,
collaboration_rate: totalActivities > 0
? ((metrics.contactsEngaged.size + metrics.jobsCollaborated.size) / totalActivities) * 100
: 0,
});
}
// Workload distribution
if (includeWorkload) {
// Estimate active jobs (jobs created in period)
const activeJobs = metrics.jobsCreated;
const pendingTasks = totalActivities - metrics.tasksCompleted;
const openEstimates = metrics.estimatesCreated;
const totalWorkload = activeJobs + pendingTasks + openEstimates;
const capacityUtilization = Math.min((totalWorkload / 30) * 100, 100); // 30 items = 100% capacity
const workloadStatus = capacityUtilization >= 90 ? 'Overloaded' :
capacityUtilization >= 50 ? 'Optimal' : 'Underutilized';
workloadDistribution.push({
user_id: userId,
user_name: user.display_name || user.name || 'Unknown',
active_jobs: activeJobs,
pending_tasks: pendingTasks,
open_estimates: openEstimates,
total_workload: totalWorkload,
capacity_utilization: capacityUtilization,
workload_status: workloadStatus,
});
}
}
// Sort by productivity score
userProductivityMetrics.sort((a, b) => b.productivity_score - a.productivity_score);
// Team metrics
const teamMetrics = {
total_team_members: userMetricsMap.size,
active_members: userProductivityMetrics.length,
inactive_members: userMetricsMap.size - userProductivityMetrics.length,
avg_productivity_score: userProductivityMetrics.length > 0
? userProductivityMetrics.reduce((sum, u) => sum + u.productivity_score, 0) / userProductivityMetrics.length
: 0,
total_team_activities: userProductivityMetrics.reduce((sum, u) => sum + u.total_activities, 0),
avg_activities_per_member: userProductivityMetrics.length > 0
? userProductivityMetrics.reduce((sum, u) => sum + u.total_activities, 0) / userProductivityMetrics.length
: 0,
top_performer_id: userProductivityMetrics[0]?.user_id || '',
top_performer_name: userProductivityMetrics[0]?.user_name || 'N/A',
};
// Performance comparison
const performanceComparison = [
{
metric: 'Productivity Score',
team_average: teamMetrics.avg_productivity_score,
top_performer_value: userProductivityMetrics[0]?.productivity_score || 0,
bottom_performer_value: userProductivityMetrics[userProductivityMetrics.length - 1]?.productivity_score || 0,
performance_gap: (userProductivityMetrics[0]?.productivity_score || 0) -
(userProductivityMetrics[userProductivityMetrics.length - 1]?.productivity_score || 0),
},
{
metric: 'Total Activities',
team_average: teamMetrics.avg_activities_per_member,
top_performer_value: Math.max(...userProductivityMetrics.map(u => u.total_activities), 0),
bottom_performer_value: Math.min(...userProductivityMetrics.map(u => u.total_activities), 0),
performance_gap: Math.max(...userProductivityMetrics.map(u => u.total_activities), 0) -
Math.min(...userProductivityMetrics.map(u => u.total_activities), 0),
},
];
// Recommendations
const recommendations = [];
if (teamMetrics.avg_productivity_score < 60) {
recommendations.push(`⚠️ Team average productivity score is low (${teamMetrics.avg_productivity_score.toFixed(1)}/100) - review workflows and processes`);
}
const overloadedUsers = workloadDistribution.filter(w => w.workload_status === 'Overloaded').length;
if (overloadedUsers > 0) {
recommendations.push(`🚨 ${overloadedUsers} team member(s) overloaded - redistribute workload`);
}
const underutilizedUsers = workloadDistribution.filter(w => w.workload_status === 'Underutilized').length;
if (underutilizedUsers > 0) {
recommendations.push(`📊 ${underutilizedUsers} team member(s) underutilized - optimize task assignments`);
}
if (userProductivityMetrics[0] && userProductivityMetrics[0].productivity_score >= 80) {
recommendations.push(`🏆 Top performer: ${userProductivityMetrics[0].user_name} (${userProductivityMetrics[0].productivity_score}/100)`);
}
const lowCollaborators = collaborationMetrics.filter(c => c.collaboration_rate < 30).length;
if (lowCollaborators > 0) {
recommendations.push(`🤝 ${lowCollaborators} team member(s) with low collaboration - encourage team interaction`);
}
return {
data_source: 'Live JobNimbus API data',
analysis_timestamp: new Date().toISOString(),
analysis_period_days: daysBack,
team_metrics: teamMetrics,
user_productivity_metrics: userProductivityMetrics,
activity_patterns: includePatterns ? activityPatterns : undefined,
collaboration_metrics: includeCollaboration ? collaborationMetrics : undefined,
workload_distribution: includeWorkload ? workloadDistribution : undefined,
performance_comparison: performanceComparison,
recommendations: recommendations,
key_insights: [
`Team average productivity: ${teamMetrics.avg_productivity_score.toFixed(1)}/100`,
`${teamMetrics.active_members} active members, ${teamMetrics.inactive_members} inactive`,
`Top performer: ${teamMetrics.top_performer_name}`,
`Average ${teamMetrics.avg_activities_per_member.toFixed(0)} activities per member`,
],
};
}
catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 'Failed',
};
}
}
/**
* Calculate productivity score
*/
calculateProductivityScore(jobsCreated, contactsCreated, estimatesCreated, tasksCompleted, totalActivities, avgResponseTime) {
let score = 0;
// Volume (40 points)
const volumeScore = Math.min((jobsCreated * 3 + contactsCreated * 2 + estimatesCreated * 4 + tasksCompleted * 2) / 2, 40);
score += volumeScore;
// Activity frequency (30 points)
const activityScore = Math.min(totalActivities * 0.5, 30);
score += activityScore;
// Efficiency (30 points) - faster response time is better
const efficiencyScore = avgResponseTime > 0
? Math.max(0, 30 - (avgResponseTime / 10))
: 15; // Default if no data
score += Math.min(efficiencyScore, 30);
return Math.min(Math.round(score), 100);
}
/**
* Calculate workload balance
*/
calculateWorkloadBalance(jobsCreated, totalActivities) {
if (totalActivities === 0)
return 0;
const jobRatio = jobsCreated / totalActivities;
const idealRatio = 0.3; // 30% of activities should be job creation
const deviation = Math.abs(jobRatio - idealRatio);
const balance = Math.max(0, 100 - (deviation * 200));
return Math.round(balance);
}
/**
* Calculate collaboration score
*/
calculateCollaborationScore(contactsEngaged, jobsCollaborated, totalActivities) {
if (totalActivities === 0)
return 0;
const collaborationRatio = (contactsEngaged + jobsCollaborated) / totalActivities;
const score = Math.min(collaborationRatio * 100, 100);
return Math.round(score);
}
}
//# sourceMappingURL=getUserProductivityAnalytics.js.map