UNPKG

jobnimbus-mcp-client

Version:

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

414 lines 20.7 kB
/** * 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