jobnimbus-mcp-client
Version:
JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server
414 lines • 20.7 kB
JavaScript
/**
* Get Task Management Analytics
* Comprehensive task analytics with priority tracking, assignment analysis, completion metrics, and productivity insights
*/
import { BaseTool } from '../baseTool.js';
export class GetTaskManagementAnalyticsTool extends BaseTool {
get definition() {
return {
name: 'get_task_management_analytics',
description: 'Comprehensive task analytics with priority tracking, assignment analysis, completion metrics, overdue detection, and productivity insights',
inputSchema: {
type: 'object',
properties: {
assignee_filter: {
type: 'string',
description: 'Filter by assignee name or ID',
},
priority_filter: {
type: 'string',
description: 'Filter by priority level',
},
include_overdue_analysis: {
type: 'boolean',
default: true,
description: 'Include detailed overdue task analysis',
},
include_productivity_trends: {
type: 'boolean',
default: true,
description: 'Include productivity trend analysis',
},
days_back: {
type: 'number',
default: 30,
description: 'Days of history to analyze (default: 30)',
},
},
},
};
}
async execute(input, context) {
try {
const assigneeFilter = input.assignee_filter;
const priorityFilter = input.priority_filter;
const includeOverdue = input.include_overdue_analysis !== false;
const includeProductivity = input.include_productivity_trends !== false;
const daysBack = input.days_back || 30;
// Fetch tasks and related data
const [activitiesResponse] = await Promise.all([
this.client.get(context.apiKey, 'activities', { size: 100 }),
]);
// Note: Tasks might come from activities endpoint in JobNimbus
const activities = activitiesResponse.data?.activity || [];
// 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 - task management analysis will be limited');
}
// Filter task-type activities
const tasks = activities.filter((act) => {
const type = (act.activity_type || act.type || '').toLowerCase();
return type.includes('task') || type.includes('todo') || type.includes('action');
});
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);
}
}
// Overall task metrics
const metrics = {
total_tasks: 0,
completed_tasks: 0,
pending_tasks: 0,
overdue_tasks: 0,
completion_rate: 0,
avg_completion_time_hours: 0,
tasks_with_due_dates: 0,
};
const completionTimes = [];
const priorityMap = new Map();
const assigneeMap = new Map();
const typeMap = new Map();
const overdueTasks = [];
// Process tasks
for (const task of tasks) {
// Apply filters
if (assigneeFilter) {
const assigneeId = task.assigned_to || task.assignee_id || '';
const assigneeName = this.getAssigneeName(task, userLookup);
if (!assigneeId.includes(assigneeFilter) && !assigneeName.toLowerCase().includes(assigneeFilter.toLowerCase())) {
continue;
}
}
const priority = task.priority || 'Normal';
if (priorityFilter && priority.toLowerCase() !== priorityFilter.toLowerCase()) {
continue;
}
const createdDate = task.date_created || task.created_at || 0;
if (createdDate < cutoffDate)
continue;
metrics.total_tasks++;
// Completion status
const statusName = (task.status_name || task.status || '').toLowerCase();
const isCompleted = statusName.includes('complete') ||
statusName.includes('done') ||
statusName.includes('closed');
if (isCompleted) {
metrics.completed_tasks++;
// Calculate completion time
const completedDate = task.date_completed || task.date_updated || 0;
if (completedDate > 0 && createdDate > 0) {
const completionTime = (completedDate - createdDate) / (1000 * 60 * 60); // hours
completionTimes.push(completionTime);
}
}
else {
metrics.pending_tasks++;
}
// Due date tracking
const dueDate = task.date_end || task.due_date || 0;
if (dueDate > 0) {
metrics.tasks_with_due_dates++;
// Check if overdue
if (!isCompleted && dueDate < now) {
metrics.overdue_tasks++;
if (includeOverdue) {
const daysOverdue = Math.floor((now - dueDate) / (1000 * 60 * 60 * 24));
const urgencyScore = this.calculateUrgencyScore(priority, daysOverdue);
overdueTasks.push({
task_id: task.jnid || task.id || 'unknown',
task_name: task.name || task.title || 'Unnamed Task',
assignee: this.getAssigneeName(task, userLookup),
days_overdue: daysOverdue,
priority: priority,
created_date: createdDate > 0 ? new Date(createdDate).toISOString() : 'Unknown',
due_date: new Date(dueDate).toISOString(),
urgency_score: urgencyScore,
});
}
}
}
// Priority breakdown
if (!priorityMap.has(priority)) {
priorityMap.set(priority, { total: 0, completed: 0, overdue: 0, ages: [] });
}
const priorityData = priorityMap.get(priority);
priorityData.total++;
if (isCompleted)
priorityData.completed++;
if (!isCompleted && dueDate > 0 && dueDate < now)
priorityData.overdue++;
const ageHours = (now - createdDate) / (1000 * 60 * 60);
priorityData.ages.push(ageHours / 24); // days
// Assignee breakdown
const assigneeId = task.assigned_to || task.assignee_id || 'unassigned';
const assigneeName = assigneeId === 'unassigned' ? 'Unassigned' : this.getAssigneeName(task, userLookup);
if (!assigneeMap.has(assigneeId)) {
assigneeMap.set(assigneeId, {
name: assigneeName,
total: 0,
completed: 0,
pending: 0,
overdue: 0,
completionTimes: [],
});
}
const assigneeData = assigneeMap.get(assigneeId);
assigneeData.total++;
if (isCompleted) {
assigneeData.completed++;
if (completionTimes.length > 0) {
assigneeData.completionTimes.push(completionTimes[completionTimes.length - 1]);
}
}
else {
assigneeData.pending++;
if (dueDate > 0 && dueDate < now) {
assigneeData.overdue++;
}
}
// Task type breakdown
const taskType = task.activity_type || task.type || 'General';
if (!typeMap.has(taskType)) {
typeMap.set(taskType, { count: 0, completed: 0, completionTimes: [] });
}
const typeData = typeMap.get(taskType);
typeData.count++;
if (isCompleted) {
typeData.completed++;
if (completionTimes.length > 0) {
typeData.completionTimes.push(completionTimes[completionTimes.length - 1]);
}
}
}
// Calculate metrics
metrics.completion_rate = metrics.total_tasks > 0
? (metrics.completed_tasks / metrics.total_tasks) * 100
: 0;
metrics.avg_completion_time_hours = completionTimes.length > 0
? completionTimes.reduce((sum, t) => sum + t, 0) / completionTimes.length
: 0;
// Priority breakdown
const priorityBreakdown = [];
for (const [priority, data] of priorityMap.entries()) {
priorityBreakdown.push({
priority_level: priority,
task_count: data.total,
completed_count: data.completed,
overdue_count: data.overdue,
completion_rate: data.total > 0 ? (data.completed / data.total) * 100 : 0,
avg_age_days: data.ages.length > 0
? data.ages.reduce((sum, age) => sum + age, 0) / data.ages.length
: 0,
});
}
priorityBreakdown.sort((a, b) => b.task_count - a.task_count);
// Assignment analytics
const assignmentAnalytics = [];
for (const [assigneeId, data] of assigneeMap.entries()) {
const completionRate = data.total > 0 ? (data.completed / data.total) * 100 : 0;
const avgCompletionTime = data.completionTimes.length > 0
? data.completionTimes.reduce((sum, t) => sum + t, 0) / data.completionTimes.length
: 0;
const productivityScore = this.calculateProductivityScore(completionRate, avgCompletionTime, data.overdue, data.total);
const workloadStatus = data.pending >= 20 ? 'Overloaded' :
data.pending >= 5 ? 'Optimal' : 'Underutilized';
assignmentAnalytics.push({
assignee_name: data.name,
assignee_id: assigneeId,
total_tasks: data.total,
completed_tasks: data.completed,
pending_tasks: data.pending,
overdue_tasks: data.overdue,
completion_rate: completionRate,
avg_completion_time_hours: avgCompletionTime,
productivity_score: productivityScore,
workload_status: workloadStatus,
});
}
assignmentAnalytics.sort((a, b) => b.productivity_score - a.productivity_score);
// Task type metrics
const taskTypeMetrics = [];
for (const [type, data] of typeMap.entries()) {
taskTypeMetrics.push({
task_type: type,
count: data.count,
completed: data.completed,
avg_completion_time_hours: data.completionTimes.length > 0
? data.completionTimes.reduce((sum, t) => sum + t, 0) / data.completionTimes.length
: 0,
completion_rate: data.count > 0 ? (data.completed / data.count) * 100 : 0,
});
}
taskTypeMetrics.sort((a, b) => b.count - a.count);
// Sort overdue tasks by urgency
overdueTasks.sort((a, b) => b.urgency_score - a.urgency_score);
// Productivity trends (if requested)
const productivityTrends = [];
if (includeProductivity) {
// Weekly trends for the past 4 weeks
for (let week = 0; week < 4; week++) {
const weekStart = now - ((week + 1) * 7 * 24 * 60 * 60 * 1000);
const weekEnd = now - (week * 7 * 24 * 60 * 60 * 1000);
const weekTasks = tasks.filter((t) => {
const created = t.date_created || t.created_at || 0;
return created >= weekStart && created < weekEnd;
});
const weekCompleted = weekTasks.filter((t) => {
const status = (t.status_name || t.status || '').toLowerCase();
return status.includes('complete') || status.includes('done');
});
const weekCompletionTimes = [];
for (const task of weekCompleted) {
const created = task.date_created || task.created_at || 0;
const completed = task.date_completed || task.date_updated || 0;
if (created > 0 && completed > 0) {
weekCompletionTimes.push((completed - created) / (1000 * 60 * 60));
}
}
const completionRate = weekTasks.length > 0
? (weekCompleted.length / weekTasks.length) * 100
: 0;
const avgTime = weekCompletionTimes.length > 0
? weekCompletionTimes.reduce((sum, t) => sum + t, 0) / weekCompletionTimes.length
: 0;
productivityTrends.unshift({
period: `Week ${4 - week}`,
tasks_created: weekTasks.length,
tasks_completed: weekCompleted.length,
completion_rate: completionRate,
avg_completion_time_hours: avgTime,
trend: 'Stable', // Will be calculated after all weeks
});
}
// Calculate trends
for (let i = 1; i < productivityTrends.length; i++) {
const current = productivityTrends[i];
const previous = productivityTrends[i - 1];
if (current.completion_rate > previous.completion_rate + 5) {
current.trend = 'Improving';
}
else if (current.completion_rate < previous.completion_rate - 5) {
current.trend = 'Declining';
}
else {
current.trend = 'Stable';
}
}
}
// Generate recommendations
const recommendations = [];
if (metrics.overdue_tasks > metrics.total_tasks * 0.2) {
recommendations.push(`🚨 High overdue rate (${((metrics.overdue_tasks / metrics.total_tasks) * 100).toFixed(1)}%) - review workload and priorities`);
}
if (metrics.completion_rate < 60) {
recommendations.push(`⚠️ Low completion rate (${metrics.completion_rate.toFixed(1)}%) - consider task prioritization review`);
}
const overloadedAssignees = assignmentAnalytics.filter(a => a.workload_status === 'Overloaded').length;
if (overloadedAssignees > 0) {
recommendations.push(`👥 ${overloadedAssignees} team member(s) overloaded - redistribute tasks`);
}
const topPerformer = assignmentAnalytics[0];
if (topPerformer && topPerformer.productivity_score >= 80) {
recommendations.push(`🏆 Top performer: ${topPerformer.assignee_name} (${topPerformer.productivity_score}/100 productivity score)`);
}
if (metrics.avg_completion_time_hours > 72) {
recommendations.push(`⏱️ High average completion time (${(metrics.avg_completion_time_hours / 24).toFixed(1)} days) - streamline workflows`);
}
return {
data_source: 'Live JobNimbus API data',
analysis_timestamp: new Date().toISOString(),
analysis_period_days: daysBack,
summary: metrics,
priority_breakdown: priorityBreakdown,
assignment_analytics: assignmentAnalytics,
task_type_metrics: taskTypeMetrics,
overdue_analysis: includeOverdue ? {
total_overdue: overdueTasks.length,
critical_overdue: overdueTasks.filter(t => t.urgency_score >= 80).length,
top_overdue_tasks: overdueTasks.slice(0, 10),
} : undefined,
productivity_trends: includeProductivity ? productivityTrends : undefined,
recommendations: recommendations,
key_insights: [
`Overall completion rate: ${metrics.completion_rate.toFixed(1)}%`,
`${metrics.overdue_tasks} task(s) overdue out of ${metrics.total_tasks} total`,
`Average completion time: ${(metrics.avg_completion_time_hours / 24).toFixed(1)} days`,
`${assignmentAnalytics.length} team member(s) tracked`,
],
};
}
catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 'Failed',
};
}
}
/**
* Get assignee name from task and user lookup
*/
getAssigneeName(task, userLookup) {
const assigneeId = task.assigned_to || task.assignee_id;
if (!assigneeId)
return 'Unassigned';
const user = userLookup.get(assigneeId);
if (user) {
return user.display_name || user.name || user.email || assigneeId;
}
return task.assignee_name || assigneeId;
}
/**
* Calculate urgency score for overdue task
*/
calculateUrgencyScore(priority, daysOverdue) {
const priorityLower = priority.toLowerCase();
let baseScore = 50;
if (priorityLower.includes('high') || priorityLower.includes('urgent')) {
baseScore = 80;
}
else if (priorityLower.includes('low')) {
baseScore = 30;
}
// Add points for days overdue (max 20 points)
const overduePoints = Math.min(daysOverdue * 2, 20);
return Math.min(baseScore + overduePoints, 100);
}
/**
* Calculate productivity score for assignee
*/
calculateProductivityScore(completionRate, avgCompletionTime, overdueCount, totalTasks) {
let score = 0;
// Completion rate (50 points)
score += (completionRate / 100) * 50;
// Speed (25 points) - faster is better
const speedScore = avgCompletionTime > 0 ? Math.max(0, 25 - (avgCompletionTime / 10)) : 0;
score += Math.min(speedScore, 25);
// Quality (25 points) - fewer overdue is better
const overdueRate = totalTasks > 0 ? (overdueCount / totalTasks) * 100 : 0;
const qualityScore = Math.max(0, 25 - (overdueRate / 4));
score += qualityScore;
return Math.min(Math.round(score), 100);
}
}
//# sourceMappingURL=getTaskManagementAnalytics.js.map