jobnimbus-mcp-client
Version:
JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server
482 lines • 25.1 kB
JavaScript
/**
* Get Project Management Analytics
* Comprehensive project tracking with milestone analysis, timeline adherence, resource allocation, and risk assessment
*/
import { BaseTool } from '../baseTool.js';
export class GetProjectManagementAnalyticsTool extends BaseTool {
get definition() {
return {
name: 'get_project_management_analytics',
description: 'Comprehensive project management analytics with milestone tracking, timeline adherence, resource allocation, budget monitoring, risk assessment, and optimization recommendations',
inputSchema: {
type: 'object',
properties: {
status_filter: {
type: 'string',
enum: ['active', 'completed', 'all'],
default: 'active',
description: 'Filter projects by status',
},
include_timeline_analysis: {
type: 'boolean',
default: true,
description: 'Include timeline trend analysis',
},
include_resource_analysis: {
type: 'boolean',
default: true,
description: 'Include resource allocation analysis',
},
health_threshold: {
type: 'number',
default: 70,
description: 'Health score threshold for at-risk projects (default: 70)',
},
days_back: {
type: 'number',
default: 90,
description: 'Days of history to analyze (default: 90)',
},
},
},
};
}
async execute(input, context) {
try {
const statusFilter = input.status_filter || 'active';
// const includeTimelineAnalysis = input.include_timeline_analysis !== false;
const includeResourceAnalysis = input.include_resource_analysis !== false;
const healthThreshold = input.health_threshold || 70;
const daysBack = input.days_back || 90;
// 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 - project management analysis will be limited');
}
const now = Date.now();
const cutoffDate = now - (daysBack * 24 * 60 * 60 * 1000);
// User map
const userMap = new Map();
for (const user of users) {
const userId = user.jnid || user.id;
if (userId)
userMap.set(userId, user);
}
// Build project tracking map
const projectMap = new Map();
// Initialize projects (using jobs as projects)
for (const job of jobs) {
const jobId = job.jnid || job.id;
if (!jobId)
continue;
const createdDate = job.date_created || job.created_at || 0;
if (createdDate < cutoffDate)
continue;
projectMap.set(jobId, {
job: job,
activities: [],
milestones: 0,
completedMilestones: 0,
teamMembers: new Set(),
estimate: null,
});
}
// Add activities to projects
for (const activity of activities) {
const related = activity.related || [];
const jobRel = related.find((r) => r.type === 'job');
if (!jobRel || !jobRel.id)
continue;
const jobId = jobRel.id;
if (!projectMap.has(jobId))
continue;
const project = projectMap.get(jobId);
project.activities.push(activity);
// Track milestones (tasks with specific types)
const activityType = (activity.type || '').toLowerCase();
if (activityType.includes('milestone') || activityType.includes('phase') || activityType.includes('review')) {
project.milestones++;
const status = (activity.status || '').toLowerCase();
if (status.includes('complete') || status.includes('done')) {
project.completedMilestones++;
}
}
// Track team members
const assigneeId = activity.assigned_to || activity.owner_id;
if (assigneeId) {
project.teamMembers.add(assigneeId);
}
}
// Add estimates to projects
for (const estimate of estimates) {
const related = estimate.related || [];
const jobRel = related.find((r) => r.type === 'job');
if (!jobRel || !jobRel.id)
continue;
const jobId = jobRel.id;
if (!projectMap.has(jobId)) {
const project = projectMap.get(jobId);
if (project) {
project.estimate = estimate;
}
}
}
// Analyze projects
const projectDetails = [];
let totalCompleted = 0;
let totalDelayed = 0;
let totalOnSchedule = 0;
let totalValue = 0;
let completionDurations = [];
for (const [jobId, project] of projectMap.entries()) {
const job = project.job;
const status = (job.status_name || '').toLowerCase();
const isCompleted = status.includes('complete') || status.includes('won') || status.includes('closed');
const isActive = !status.includes('cancelled') && !isCompleted;
// Status filter
if (statusFilter === 'active' && !isActive)
continue;
if (statusFilter === 'completed' && !isCompleted)
continue;
// Dates
const startDate = job.date_start || job.date_created || 0;
const scheduledEndDate = job.date_end || 0;
const actualEndDate = job.date_status_change || job.date_updated || 0;
// Duration
const duration = isCompleted
? (actualEndDate - startDate) / (1000 * 60 * 60 * 24)
: (now - startDate) / (1000 * 60 * 60 * 24);
if (isCompleted && startDate > 0 && actualEndDate > 0) {
completionDurations.push(duration);
totalCompleted++;
}
// Timeline status
let timelineStatus;
if (isCompleted) {
if (scheduledEndDate > 0 && actualEndDate > scheduledEndDate) {
timelineStatus = 'Completed Late';
totalDelayed++;
}
else {
timelineStatus = 'Completed On Time';
}
}
else {
if (scheduledEndDate > 0) {
const daysUntilDeadline = (scheduledEndDate - now) / (1000 * 60 * 60 * 24);
if (daysUntilDeadline < 0) {
timelineStatus = 'Delayed';
totalDelayed++;
}
else if (daysUntilDeadline < 7) {
timelineStatus = 'At Risk';
}
else {
timelineStatus = 'On Schedule';
totalOnSchedule++;
}
}
else {
timelineStatus = 'On Schedule';
totalOnSchedule++;
}
}
// Budget
const budget = project.estimate ? parseFloat(project.estimate.total || 0) : 0;
const actualCost = parseFloat(job.total || job.value || 0);
const budgetVariance = budget > 0 ? ((actualCost - budget) / budget) * 100 : 0;
totalValue += actualCost;
// Completion percentage
const totalMilestones = Math.max(project.milestones, 1);
const completionPercentage = (project.completedMilestones / totalMilestones) * 100;
// Health score (0-100)
let healthScore = 0;
// Timeline component (40 points)
if (timelineStatus === 'On Schedule' || timelineStatus === 'Completed On Time') {
healthScore += 40;
}
else if (timelineStatus === 'At Risk') {
healthScore += 25;
}
else if (timelineStatus === 'Delayed' || timelineStatus === 'Completed Late') {
healthScore += 10;
}
// Budget component (30 points)
if (budgetVariance <= 0) {
healthScore += 30;
}
else if (budgetVariance <= 10) {
healthScore += 20;
}
else if (budgetVariance <= 25) {
healthScore += 10;
}
// Progress component (30 points)
healthScore += Math.min(completionPercentage * 0.3, 30);
// Risk level
const riskLevel = healthScore >= 80 ? 'Low' :
healthScore >= 60 ? 'Medium' :
healthScore >= 40 ? 'High' : 'Critical';
// Recommended action
const recommendedAction = riskLevel === 'Critical' ? 'Immediate intervention required - escalate to management' :
riskLevel === 'High' ? 'Review resource allocation and timeline' :
riskLevel === 'Medium' ? 'Monitor closely and adjust if needed' :
'Continue current approach';
// Customer name
const customerRel = (job.related || []).find((r) => r.type === 'contact');
const customerName = customerRel?.display_name || 'Unknown';
projectDetails.push({
project_id: jobId,
project_name: job.display_name || job.name || 'Unnamed Project',
customer_name: customerName,
status: job.status_name || 'Unknown',
start_date: startDate > 0 ? new Date(startDate).toISOString().split('T')[0] : 'N/A',
scheduled_end_date: scheduledEndDate > 0 ? new Date(scheduledEndDate).toISOString().split('T')[0] : 'N/A',
actual_end_date: isCompleted && actualEndDate > 0 ? new Date(actualEndDate).toISOString().split('T')[0] : null,
duration_days: Math.round(duration),
completion_percentage: Math.round(completionPercentage),
timeline_status: timelineStatus,
budget: budget,
actual_cost: actualCost,
budget_variance: budgetVariance,
assigned_team_size: project.teamMembers.size,
milestones_completed: project.completedMilestones,
total_milestones: project.milestones,
health_score: Math.round(healthScore),
risk_level: riskLevel,
recommended_action: recommendedAction,
});
}
// Sort by health score (lowest first - most at risk)
projectDetails.sort((a, b) => a.health_score - b.health_score);
// Project metrics
const activeProjects = projectDetails.filter(p => !['Completed On Time', 'Completed Late'].includes(p.timeline_status)).length;
const avgCompletionRate = projectDetails.length > 0
? projectDetails.reduce((sum, p) => sum + p.completion_percentage, 0) / projectDetails.length
: 0;
const avgProjectDuration = completionDurations.length > 0
? completionDurations.reduce((sum, d) => sum + d, 0) / completionDurations.length
: 0;
const onTimeDeliveryRate = totalCompleted > 0
? ((totalCompleted - totalDelayed) / totalCompleted) * 100
: 0;
const projectMetrics = {
total_projects: projectDetails.length,
active_projects: activeProjects,
completed_projects: totalCompleted,
delayed_projects: totalDelayed,
on_schedule_projects: totalOnSchedule,
avg_completion_rate: avgCompletionRate,
avg_project_duration_days: avgProjectDuration,
total_project_value: totalValue,
on_time_delivery_rate: onTimeDeliveryRate,
};
// Milestone analysis
const milestoneTypes = new Map();
for (const activity of activities) {
const activityType = (activity.type || 'General').trim();
const isMilestone = activityType.toLowerCase().includes('milestone') ||
activityType.toLowerCase().includes('phase') ||
activityType.toLowerCase().includes('review');
if (!isMilestone)
continue;
if (!milestoneTypes.has(activityType)) {
milestoneTypes.set(activityType, { total: 0, completed: 0, durations: [] });
}
const data = milestoneTypes.get(activityType);
data.total++;
const status = (activity.status || '').toLowerCase();
if (status.includes('complete') || status.includes('done')) {
data.completed++;
const dateStart = activity.date_start || activity.date_created || 0;
const dateEnd = activity.date_end || activity.date_updated || 0;
if (dateStart > 0 && dateEnd > 0 && dateEnd > dateStart) {
const duration = (dateEnd - dateStart) / (1000 * 60 * 60 * 24);
data.durations.push(duration);
}
}
}
const milestoneAnalyses = [];
for (const [type, data] of milestoneTypes.entries()) {
const completionRate = data.total > 0 ? (data.completed / data.total) * 100 : 0;
const avgCompletionTime = data.durations.length > 0
? data.durations.reduce((sum, d) => sum + d, 0) / data.durations.length
: 0;
const delayedCount = data.total - data.completed;
const onTimeRate = data.total > 0 ? (data.completed / data.total) * 100 : 0;
milestoneAnalyses.push({
milestone_type: type,
total_milestones: data.total,
completed_milestones: data.completed,
completion_rate: completionRate,
avg_completion_time_days: avgCompletionTime,
delayed_count: delayedCount,
on_time_rate: onTimeRate,
});
}
milestoneAnalyses.sort((a, b) => b.total_milestones - a.total_milestones);
// Resource allocation analysis
const resourceAllocations = [];
if (includeResourceAnalysis) {
const resourceMap = new Map();
for (const [jobId, project] of projectMap.entries()) {
const projectDetail = projectDetails.find(p => p.project_id === jobId);
if (!projectDetail)
continue;
for (const memberId of project.teamMembers) {
if (!resourceMap.has(memberId)) {
resourceMap.set(memberId, {
projects: new Set(),
activeProjects: 0,
completedProjects: 0,
healthScores: [],
onTimeDeliveries: 0,
totalDeliveries: 0,
});
}
const resource = resourceMap.get(memberId);
resource.projects.add(jobId);
resource.healthScores.push(projectDetail.health_score);
if (['Completed On Time', 'Completed Late'].includes(projectDetail.timeline_status)) {
resource.completedProjects++;
resource.totalDeliveries++;
if (projectDetail.timeline_status === 'Completed On Time') {
resource.onTimeDeliveries++;
}
}
else {
resource.activeProjects++;
}
}
}
for (const [userId, data] of resourceMap.entries()) {
const user = userMap.get(userId);
const userName = user ? `${user.first_name || ''} ${user.last_name || ''}`.trim() : 'Unknown';
const avgHealthScore = data.healthScores.length > 0
? data.healthScores.reduce((sum, s) => sum + s, 0) / data.healthScores.length
: 0;
const onTimeDeliveryRate = data.totalDeliveries > 0
? (data.onTimeDeliveries / data.totalDeliveries) * 100
: 0;
// Capacity utilization (assuming max 5 active projects)
const capacityUtilization = Math.min((data.activeProjects / 5) * 100, 100);
const performanceRating = avgHealthScore >= 80 && onTimeDeliveryRate >= 90 ? 'Excellent' :
avgHealthScore >= 70 && onTimeDeliveryRate >= 75 ? 'Good' :
avgHealthScore >= 60 && onTimeDeliveryRate >= 60 ? 'Fair' : 'Needs Improvement';
resourceAllocations.push({
resource_name: userName,
resource_id: userId,
assigned_projects: data.projects.size,
active_projects: data.activeProjects,
completed_projects: data.completedProjects,
on_time_delivery_rate: onTimeDeliveryRate,
avg_project_health_score: avgHealthScore,
capacity_utilization: capacityUtilization,
performance_rating: performanceRating,
});
}
resourceAllocations.sort((a, b) => b.avg_project_health_score - a.avg_project_health_score);
}
// Risk assessments
const riskAssessments = [];
// Schedule risk
const scheduleRiskProjects = projectDetails.filter(p => ['At Risk', 'Delayed'].includes(p.timeline_status));
if (scheduleRiskProjects.length > 0) {
riskAssessments.push({
risk_category: 'Schedule Risk',
projects_at_risk: scheduleRiskProjects.length,
total_value_at_risk: scheduleRiskProjects.reduce((sum, p) => sum + p.actual_cost, 0),
severity: scheduleRiskProjects.length > 5 ? 'Critical' : scheduleRiskProjects.length > 3 ? 'High' : 'Medium',
mitigation_actions: [
'Reallocate resources to critical path tasks',
'Extend deadlines for non-critical milestones',
'Increase team size for delayed projects',
'Implement daily stand-ups for at-risk projects',
],
priority: 1,
});
}
// Budget risk
const budgetRiskProjects = projectDetails.filter(p => p.budget_variance > 10);
if (budgetRiskProjects.length > 0) {
riskAssessments.push({
risk_category: 'Budget Risk',
projects_at_risk: budgetRiskProjects.length,
total_value_at_risk: budgetRiskProjects.reduce((sum, p) => sum + Math.abs(p.budget - p.actual_cost), 0),
severity: budgetRiskProjects.length > 3 ? 'High' : 'Medium',
mitigation_actions: [
'Review and approve all change orders',
'Implement stricter cost controls',
'Negotiate with vendors for better rates',
'Re-estimate remaining work',
],
priority: 2,
});
}
// Project recommendations
const recommendations = [];
for (const project of projectDetails.slice(0, 10)) { // Top 10 most at-risk
if (project.health_score >= healthThreshold)
continue;
if (project.timeline_status === 'Delayed' || project.timeline_status === 'At Risk') {
recommendations.push({
project_id: project.project_id,
project_name: project.project_name,
recommendation_type: 'Resource Adjustment',
urgency: project.timeline_status === 'Delayed' ? 'Immediate' : 'High',
description: `Add ${Math.max(1, Math.ceil(project.assigned_team_size * 0.5))} additional team member(s)`,
expected_impact: 'Reduce timeline by 20-30%',
});
}
if (project.budget_variance > 25) {
recommendations.push({
project_id: project.project_id,
project_name: project.project_name,
recommendation_type: 'Budget Review',
urgency: 'High',
description: `Budget variance is ${project.budget_variance.toFixed(1)}% - review and adjust`,
expected_impact: 'Prevent further cost overruns',
});
}
}
return {
data_source: 'Live JobNimbus API data',
analysis_timestamp: new Date().toISOString(),
analysis_period_days: daysBack,
project_metrics: projectMetrics,
project_details: projectDetails,
milestone_analysis: milestoneAnalyses,
resource_allocations: includeResourceAnalysis ? resourceAllocations : undefined,
risk_assessments: riskAssessments,
recommendations: recommendations,
key_insights: [
`${activeProjects} active project(s) being tracked`,
`On-time delivery rate: ${onTimeDeliveryRate.toFixed(1)}%`,
`${scheduleRiskProjects.length} project(s) at schedule risk`,
`Average project health: ${projectDetails.length > 0 ? (projectDetails.reduce((sum, p) => sum + p.health_score, 0) / projectDetails.length).toFixed(1) : 0}/100`,
],
};
}
catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 'Failed',
};
}
}
}
//# sourceMappingURL=getProjectManagementAnalytics.js.map