UNPKG

jobnimbus-mcp-client

Version:

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

350 lines 19 kB
/** * Get Resource Allocation Analytics * Team resource distribution, capacity planning, utilization analysis, and optimization recommendations */ import { BaseTool } from '../baseTool.js'; export class GetResourceAllocationAnalyticsTool extends BaseTool { get definition() { return { name: 'get_resource_allocation_analytics', description: 'Team resource distribution analysis, capacity planning metrics, utilization tracking, skill allocation, and optimization recommendations', inputSchema: { type: 'object', properties: { include_capacity_planning: { type: 'boolean', default: true, description: 'Include capacity planning projections', }, include_skill_analysis: { type: 'boolean', default: true, description: 'Include skill allocation analysis', }, utilization_threshold: { type: 'number', default: 80, description: 'Utilization percentage threshold for overloaded status (default: 80)', }, days_ahead: { type: 'number', default: 30, description: 'Days to project for capacity planning (default: 30)', }, }, }, }; } async execute(input, context) { try { const includeCapacityPlanning = input.include_capacity_planning !== false; const includeSkillAnalysis = input.include_skill_analysis !== false; const utilizationThreshold = input.utilization_threshold || 80; const daysAhead = input.days_ahead || 30; // Fetch data const [jobsResponse, activitiesResponse, estimatesResponse] = await Promise.all([ this.client.get(context.apiKey, 'jobs', { size: 100 }), this.client.get(context.apiKey, 'activities', { size: 100 }), this.client.get(context.apiKey, 'estimates', { size: 100 }), ]); const jobs = jobsResponse.data?.results || []; const activities = activitiesResponse.data?.activity || []; 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 - resource allocation analysis will be limited'); } const now = Date.now(); const futureDate = now + (daysAhead * 24 * 60 * 60 * 1000); // Build user workload map const userWorkload = new Map(); // Initialize user workload for (const user of users) { const userId = user.jnid || user.id; if (!userId) continue; const userName = `${user.first_name || ''} ${user.last_name || ''}`.trim() || 'Unknown'; const role = user.role || user.position || 'Team Member'; userWorkload.set(userId, { name: userName, role: role, jobs: 0, tasks: 0, estimates: 0, activeJobs: 0, pendingEstimates: 0, upcomingActivities: 0, }); } // Count jobs by assignee for (const job of jobs) { const assigneeId = job.assigned_to || job.owner_id || ''; if (assigneeId && userWorkload.has(assigneeId)) { const workload = userWorkload.get(assigneeId); workload.jobs++; const statusLower = (job.status_name || '').toLowerCase(); if (!statusLower.includes('complete') && !statusLower.includes('cancelled')) { workload.activeJobs++; } } } // Count activities (tasks) by assignee for (const activity of activities) { const assigneeId = activity.assigned_to || activity.owner_id || ''; if (assigneeId && userWorkload.has(assigneeId)) { const workload = userWorkload.get(assigneeId); workload.tasks++; // Count upcoming activities const activityDate = activity.date_start || activity.date_created || 0; if (activityDate > now && activityDate <= futureDate) { workload.upcomingActivities++; } } } // Count estimates by assignee for (const estimate of estimates) { const assigneeId = estimate.assigned_to || estimate.owner_id || ''; if (assigneeId && userWorkload.has(assigneeId)) { const workload = userWorkload.get(assigneeId); workload.estimates++; const statusLower = (estimate.status || '').toLowerCase(); if (!statusLower.includes('approved') && !statusLower.includes('rejected')) { workload.pendingEstimates++; } } } // Calculate resource metrics const activeMembers = Array.from(userWorkload.values()).filter(w => w.jobs > 0 || w.tasks > 0 || w.estimates > 0).length; const totalWorkload = Array.from(userWorkload.values()).reduce((sum, w) => sum + w.activeJobs + w.tasks + w.pendingEstimates, 0); // Assuming 40 hours/week per team member, 8 hours/day const totalCapacityHours = userWorkload.size * 40; const allocatedHours = totalWorkload * 8; // Estimate 8 hours per task/job const availableHours = Math.max(0, totalCapacityHours - allocatedHours); const resourceMetrics = { total_team_members: userWorkload.size, active_team_members: activeMembers, total_capacity_hours: totalCapacityHours, allocated_hours: allocatedHours, available_hours: availableHours, utilization_rate: totalCapacityHours > 0 ? (allocatedHours / totalCapacityHours) * 100 : 0, avg_workload_per_member: userWorkload.size > 0 ? totalWorkload / userWorkload.size : 0, }; // Team member allocations const teamAllocations = []; for (const [userId, workload] of userWorkload.entries()) { // Workload score (weighted) const workloadScore = (workload.activeJobs * 10) + (workload.tasks * 3) + (workload.pendingEstimates * 5); // Capacity utilization (assuming 40 hours/week, max workload score ~400) const capacityUtilization = Math.min((workloadScore / 400) * 100, 100); // Utilization status const utilizationStatus = capacityUtilization >= utilizationThreshold ? 'Overloaded' : capacityUtilization >= 50 ? 'Optimal' : capacityUtilization >= 20 ? 'Underutilized' : 'Idle'; // Available capacity const availableCapacity = Math.max(0, 100 - capacityUtilization); // Recommended action const recommendedAction = utilizationStatus === 'Overloaded' ? 'Redistribute workload or hire additional support' : utilizationStatus === 'Optimal' ? 'Maintain current allocation' : utilizationStatus === 'Underutilized' ? 'Assign additional tasks or projects' : 'Engage in training or strategic initiatives'; teamAllocations.push({ member_name: workload.name, member_id: userId, role: workload.role, assigned_jobs: workload.activeJobs, assigned_tasks: workload.tasks, assigned_estimates: workload.pendingEstimates, total_workload_score: workloadScore, capacity_utilization: capacityUtilization, utilization_status: utilizationStatus, available_capacity: availableCapacity, recommended_action: recommendedAction, }); } // Sort by workload score descending teamAllocations.sort((a, b) => b.total_workload_score - a.total_workload_score); // Resource distribution const resourceDistribution = [ { resource_type: 'Active Jobs', allocated_count: jobs.filter((j) => { const status = (j.status_name || '').toLowerCase(); return !status.includes('complete') && !status.includes('cancelled'); }).length, percentage: 0, avg_value: 0, priority_level: 'High', }, { resource_type: 'Pending Estimates', allocated_count: estimates.filter((e) => { const status = (e.status || '').toLowerCase(); return !status.includes('approved') && !status.includes('rejected'); }).length, percentage: 0, avg_value: 0, priority_level: 'Medium', }, { resource_type: 'Tasks', allocated_count: activities.length, percentage: 0, avg_value: 0, priority_level: 'Medium', }, ]; const totalResources = resourceDistribution.reduce((sum, r) => sum + r.allocated_count, 0); for (const resource of resourceDistribution) { resource.percentage = totalResources > 0 ? (resource.allocated_count / totalResources) * 100 : 0; } // Capacity planning const capacityPlans = []; if (includeCapacityPlanning) { // Current week projection const currentWeekDemand = totalWorkload; const currentWeekCapacity = (userWorkload.size * 40) / 8; // 40 hours/week ÷ 8 hours/task const currentWeekGap = currentWeekDemand - currentWeekCapacity; capacityPlans.push({ time_period: 'Current Week', projected_demand: currentWeekDemand, available_capacity: currentWeekCapacity, capacity_gap: currentWeekGap, gap_percentage: currentWeekCapacity > 0 ? (currentWeekGap / currentWeekCapacity) * 100 : 0, staffing_recommendation: currentWeekGap > 0 ? `Need ${Math.ceil(currentWeekGap / 5)} additional team member(s)` : 'Current staffing adequate', hiring_priority: currentWeekGap > currentWeekCapacity * 0.5 ? 'Urgent' : currentWeekGap > currentWeekCapacity * 0.3 ? 'High' : currentWeekGap > currentWeekCapacity * 0.1 ? 'Medium' : currentWeekGap > 0 ? 'Low' : 'None', }); // Next month projection (estimate 20% growth) const nextMonthDemand = currentWeekDemand * 1.2 * 4; const nextMonthCapacity = (userWorkload.size * 160) / 8; // 160 hours/month ÷ 8 hours/task const nextMonthGap = nextMonthDemand - nextMonthCapacity; capacityPlans.push({ time_period: 'Next Month', projected_demand: nextMonthDemand, available_capacity: nextMonthCapacity, capacity_gap: nextMonthGap, gap_percentage: nextMonthCapacity > 0 ? (nextMonthGap / nextMonthCapacity) * 100 : 0, staffing_recommendation: nextMonthGap > 0 ? `Plan to hire ${Math.ceil(nextMonthGap / 20)} team member(s) within 30 days` : 'Capacity sufficient for projected growth', hiring_priority: nextMonthGap > nextMonthCapacity * 0.5 ? 'Urgent' : nextMonthGap > nextMonthCapacity * 0.3 ? 'High' : nextMonthGap > nextMonthCapacity * 0.1 ? 'Medium' : nextMonthGap > 0 ? 'Low' : 'None', }); } // Skill allocation analysis const skillAllocations = []; if (includeSkillAnalysis) { // Infer skills from job types const skillMap = new Map(); for (const job of jobs) { const jobType = job.job_type || job.type || 'General'; if (!skillMap.has(jobType)) { skillMap.set(jobType, { members: new Set(), jobs: 0 }); } const skill = skillMap.get(jobType); skill.jobs++; const assigneeId = job.assigned_to || job.owner_id; if (assigneeId) { skill.members.add(assigneeId); } } for (const [skillCategory, data] of skillMap.entries()) { const allocationRatio = data.members.size > 0 ? data.jobs / data.members.size : 0; const bottleneckRisk = data.members.size === 1 ? 'High' : data.members.size === 2 ? 'Medium' : allocationRatio > 10 ? 'Medium' : 'Low'; const recommendedTraining = []; if (bottleneckRisk === 'High') { recommendedTraining.push('Cross-train team members urgently'); recommendedTraining.push('Hire specialist for this skill'); } else if (bottleneckRisk === 'Medium') { recommendedTraining.push('Consider cross-training to reduce risk'); } skillAllocations.push({ skill_category: skillCategory, team_members_with_skill: data.members.size, jobs_requiring_skill: data.jobs, allocation_ratio: allocationRatio, bottleneck_risk: bottleneckRisk, recommended_training: recommendedTraining, }); } skillAllocations.sort((a, b) => b.allocation_ratio - a.allocation_ratio); } // Optimization opportunities const optimizations = []; // Workload rebalancing opportunity const overloadedCount = teamAllocations.filter(t => t.utilization_status === 'Overloaded').length; const underutilizedCount = teamAllocations.filter(t => t.utilization_status === 'Underutilized' || t.utilization_status === 'Idle').length; if (overloadedCount > 0 && underutilizedCount > 0) { optimizations.push({ opportunity_type: 'Workload Rebalancing', impact_level: overloadedCount > 3 ? 'Critical' : 'High', description: `${overloadedCount} overloaded team member(s) and ${underutilizedCount} underutilized member(s)`, estimated_capacity_gain: overloadedCount * 10, implementation_effort: 'Easy', priority: 1, }); } // Skill bottleneck opportunity const highRiskSkills = skillAllocations.filter(s => s.bottleneck_risk === 'High'); if (highRiskSkills.length > 0) { optimizations.push({ opportunity_type: 'Skill Diversification', impact_level: 'High', description: `${highRiskSkills.length} skill(s) with single point of failure`, estimated_capacity_gain: 15, implementation_effort: 'Moderate', priority: 2, }); } // Capacity expansion opportunity if (capacityPlans.length > 0 && capacityPlans[0].capacity_gap > 0) { optimizations.push({ opportunity_type: 'Team Expansion', impact_level: capacityPlans[0].hiring_priority === 'Urgent' ? 'Critical' : 'Medium', description: `Capacity gap of ${capacityPlans[0].capacity_gap.toFixed(1)} tasks/week`, estimated_capacity_gain: Math.ceil(capacityPlans[0].capacity_gap / 5) * 100, implementation_effort: 'Difficult', priority: 3, }); } return { data_source: 'Live JobNimbus API data', analysis_timestamp: new Date().toISOString(), resource_metrics: resourceMetrics, team_allocations: teamAllocations, resource_distribution: resourceDistribution, capacity_planning: includeCapacityPlanning ? capacityPlans : undefined, skill_allocation: includeSkillAnalysis ? skillAllocations : undefined, optimization_opportunities: optimizations, key_insights: [ `Team utilization: ${resourceMetrics.utilization_rate.toFixed(1)}%`, `${overloadedCount} overloaded, ${underutilizedCount} underutilized member(s)`, `Available capacity: ${resourceMetrics.available_hours.toFixed(0)} hours`, capacityPlans.length > 0 ? `Staffing priority: ${capacityPlans[0].hiring_priority}` : '', ].filter(Boolean), }; } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error', status: 'Failed', }; } } } //# sourceMappingURL=getResourceAllocationAnalytics.js.map