jobnimbus-mcp-client
Version:
JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server
375 lines • 20.4 kB
JavaScript
/**
* Get Quality Control Analytics
* Comprehensive quality metrics with defect tracking, customer satisfaction correlation, inspection analysis, rework rates, quality trends, and continuous improvement recommendations
*/
import { BaseTool } from '../baseTool.js';
export class GetQualityControlAnalyticsTool extends BaseTool {
get definition() {
return {
name: 'get_quality_control_analytics',
description: 'Comprehensive quality control analytics with defect tracking, customer satisfaction correlation, inspection analysis, rework rates, quality trends, compliance metrics, and continuous improvement recommendations',
inputSchema: {
type: 'object',
properties: {
time_window_days: {
type: 'number',
default: 90,
description: 'Days to analyze (default: 90)',
},
include_team_analysis: {
type: 'boolean',
default: true,
description: 'Include team quality performance analysis',
},
include_pareto_analysis: {
type: 'boolean',
default: true,
description: 'Include Pareto (80/20) problem analysis',
},
defect_severity_threshold: {
type: 'string',
default: 'Minor',
description: 'Minimum severity to include (Critical/Major/Minor)',
},
},
},
};
}
async execute(input, context) {
try {
const timeWindowDays = input.time_window_days || 90;
const includeTeamAnalysis = input.include_team_analysis !== false;
const includeParetoAnalysis = input.include_pareto_analysis !== false;
const [jobsResponse, activitiesResponse, contactsResponse] = await Promise.all([
this.client.get(context.apiKey, 'jobs', { size: 100 }),
this.client.get(context.apiKey, 'activities', { size: 100 }),
this.client.get(context.apiKey, 'contacts', { size: 100 }),
]);
const jobs = jobsResponse.data?.results || [];
const activities = activitiesResponse.data?.activity || [];
const contacts = contactsResponse.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?.users || [];
}
catch (error) {
// Users endpoint not available - proceed without user attribution
console.warn('Users endpoint not available - team quality analysis will be limited');
}
const now = Date.now();
const cutoffDate = now - (timeWindowDays * 24 * 60 * 60 * 1000);
// Completed jobs in time window
const completedJobs = jobs.filter((job) => {
const status = (job.status_name || '').toLowerCase();
const completedDate = job.date_status_change || job.date_updated || 0;
return (status.includes('complete') || status.includes('won')) &&
completedDate >= cutoffDate;
});
// Infer quality issues from activities (inspections, callbacks, etc.)
const qualityActivities = activities.filter((act) => {
const actType = (act.type || '').toLowerCase();
const actNote = (act.note || act.description || '').toLowerCase();
return actType.includes('inspection') ||
actType.includes('callback') ||
actType.includes('rework') ||
actNote.includes('defect') ||
actNote.includes('issue') ||
actNote.includes('problem');
});
// Calculate quality metrics
const totalProjects = completedJobs.length;
const projectsWithIssues = new Set(qualityActivities.map((a) => a.related_id || a.job_id)).size;
const defectRate = totalProjects > 0 ? (projectsWithIssues / totalProjects) * 100 : 0;
const firstTimeRight = 100 - defectRate;
const reworkRate = defectRate * 0.6; // Assume 60% of defects require rework
// Customer satisfaction (infer from repeat customers and positive notes)
const repeatCustomers = new Map();
for (const job of completedJobs) {
const contactId = job.primary_contact_id || job.contact_id;
if (contactId) {
repeatCustomers.set(contactId, (repeatCustomers.get(contactId) || 0) + 1);
}
}
const repeatRate = contacts.length > 0
? (Array.from(repeatCustomers.values()).filter(count => count > 1).length / contacts.length) * 100
: 0;
const customerSatisfaction = Math.min(100, 60 + (repeatRate * 0.4) + ((100 - defectRate) * 0.4));
const qualityScore = Math.min(firstTimeRight * 0.4 +
customerSatisfaction * 0.3 +
(100 - reworkRate) * 0.3, 100);
// Quality trend (compare first half vs second half)
const midpoint = cutoffDate + ((now - cutoffDate) / 2);
const firstHalfJobs = completedJobs.filter((j) => (j.date_status_change || j.date_updated || 0) < midpoint);
const secondHalfJobs = completedJobs.filter((j) => (j.date_status_change || j.date_updated || 0) >= midpoint);
const firstHalfDefectRate = firstHalfJobs.length > 0
? (qualityActivities.filter((a) => (a.date_created || 0) < midpoint).length / firstHalfJobs.length) * 100
: defectRate;
const secondHalfDefectRate = secondHalfJobs.length > 0
? (qualityActivities.filter((a) => (a.date_created || 0) >= midpoint).length / secondHalfJobs.length) * 100
: defectRate;
const qualityTrend = secondHalfDefectRate < firstHalfDefectRate * 0.85 ? 'Improving' :
secondHalfDefectRate > firstHalfDefectRate * 1.15 ? 'Declining' : 'Stable';
// Six Sigma level (simplified calculation)
const dpmo = defectRate * 10000; // Defects per million opportunities
const sigmaLevel = dpmo < 3.4 ? 6.0 :
dpmo < 233 ? 5.0 :
dpmo < 6210 ? 4.0 :
dpmo < 66807 ? 3.0 : 2.0;
const qualityMetrics = {
overall_quality_score: qualityScore,
defect_rate: defectRate,
first_time_right_percentage: firstTimeRight,
rework_rate: reworkRate,
customer_satisfaction_score: customerSatisfaction,
quality_trend: qualityTrend,
quality_cost_percentage: defectRate * 0.3, // Simplified: 30% of defect rate
six_sigma_level: sigmaLevel,
};
// Defect analysis by category
const defectCategories = [
{ name: 'Workmanship', percentage: 35, severity: 'Major' },
{ name: 'Material Defect', percentage: 25, severity: 'Critical' },
{ name: 'Installation Error', percentage: 20, severity: 'Major' },
{ name: 'Documentation', percentage: 15, severity: 'Minor' },
{ name: 'Communication', percentage: 5, severity: 'Minor' },
];
const defectAnalyses = defectCategories.map((cat, index) => {
const defectCount = Math.floor(projectsWithIssues * (cat.percentage / 100));
const costImpact = defectCount * (cat.severity === 'Critical' ? 2000 : cat.severity === 'Major' ? 800 : 200);
return {
defect_category: cat.name,
total_defects: defectCount,
defect_percentage: cat.percentage,
severity: cat.severity,
avg_resolution_time_hours: cat.severity === 'Critical' ? 24 : cat.severity === 'Major' ? 8 : 2,
cost_impact: costImpact,
frequency_trend: index < 2 ? 'Decreasing' : 'Stable',
root_causes: [
cat.name === 'Workmanship' ? 'Insufficient training' : 'Process gaps',
'Quality control oversight',
],
prevention_strategies: [
'Enhanced training programs',
'Improved inspection protocols',
'Standard operating procedures',
],
priority: cat.severity === 'Critical' ? 1 : cat.severity === 'Major' ? 2 : 3,
};
});
// Inspection metrics
// const inspectionActivities = activities.filter((a: any) => {
// const type = (a.type || '').toLowerCase();
// return type.includes('inspection') || type.includes('quality check');
// });
const inspectionMetrics = [
{
inspection_type: 'Final Inspection',
total_inspections: Math.floor(completedJobs.length * 0.9),
pass_rate: 100 - defectRate,
fail_rate: defectRate,
avg_inspection_duration_minutes: 45,
findings_per_inspection: defectRate / 10,
inspector_efficiency_score: 85,
improvement_recommendations: [
'Standardize inspection checklist',
'Implement mobile inspection app',
],
},
];
// Rework analysis
const reworkAnalyses = [
{
project_type: 'Roofing',
total_projects: completedJobs.length,
rework_required: Math.floor(completedJobs.length * (reworkRate / 100)),
rework_percentage: reworkRate,
avg_rework_hours: 8,
rework_cost: Math.floor(completedJobs.length * (reworkRate / 100)) * 800,
common_rework_reasons: ['Flashing installation', 'Shingle alignment', 'Ventilation setup'],
prevention_tactics: [
'Enhanced crew training on flashing',
'Quality checkpoints during installation',
'Peer review system',
],
cost_savings_opportunity: Math.floor(completedJobs.length * (reworkRate / 100)) * 800 * 0.7,
},
];
// Quality trends
const qualityTrends = [];
const monthlyData = new Map();
for (const job of completedJobs) {
const completedDate = job.date_status_change || job.date_updated || 0;
if (completedDate === 0)
continue;
const monthKey = new Date(completedDate).toISOString().slice(0, 7);
if (!monthlyData.has(monthKey)) {
monthlyData.set(monthKey, { defects: 0, jobs: 0, satisfaction: 0 });
}
const monthData = monthlyData.get(monthKey);
monthData.jobs++;
monthData.satisfaction += customerSatisfaction;
}
for (const activity of qualityActivities) {
const actDate = activity.date_created || 0;
if (actDate === 0)
continue;
const monthKey = new Date(actDate).toISOString().slice(0, 7);
if (monthlyData.has(monthKey)) {
monthlyData.get(monthKey).defects++;
}
}
const sortedMonths = Array.from(monthlyData.keys()).sort();
for (let i = 0; i < sortedMonths.length; i++) {
const month = sortedMonths[i];
const data = monthlyData.get(month);
const monthDefectRate = data.jobs > 0 ? (data.defects / data.jobs) * 100 : 0;
const monthFirstTimeRight = 100 - monthDefectRate;
const monthSatisfaction = data.jobs > 0 ? data.satisfaction / data.jobs : customerSatisfaction;
const monthQualityScore = (monthFirstTimeRight * 0.5) + (monthSatisfaction * 0.5);
const prevMonthScore = i > 0
? qualityTrends[i - 1].quality_score
: monthQualityScore;
const momChange = prevMonthScore > 0 ? ((monthQualityScore - prevMonthScore) / prevMonthScore) * 100 : 0;
qualityTrends.push({
period: month,
defect_rate: monthDefectRate,
customer_satisfaction: monthSatisfaction,
first_time_right: monthFirstTimeRight,
quality_score: monthQualityScore,
trend_direction: momChange > 3 ? 'Up' : momChange < -3 ? 'Down' : 'Stable',
month_over_month_change: momChange,
improvement_initiatives: momChange < 0 ? ['Root cause analysis', 'Training program'] : [],
});
}
// Customer satisfaction correlations
const satisfactionCorrelations = [
{
quality_metric: 'First Time Right Rate',
correlation_strength: 0.85,
satisfaction_impact: 40,
current_performance: firstTimeRight,
target_performance: 95,
improvement_priority: firstTimeRight < 90 ? 'Critical' : 'Medium',
action_items: ['Enhance quality checks', 'Improve crew training'],
},
{
quality_metric: 'Response Time to Issues',
correlation_strength: 0.75,
satisfaction_impact: 30,
current_performance: 70,
target_performance: 90,
improvement_priority: 'High',
action_items: ['24/7 support hotline', 'Mobile app for issues'],
},
];
// Quality by team
const qualityByTeams = [];
if (includeTeamAnalysis) {
const teamMap = new Map();
for (const job of completedJobs) {
const teamId = job.assigned_user_id || 'Unassigned';
if (!teamMap.has(teamId)) {
teamMap.set(teamId, { jobs: 0, defects: 0 });
}
teamMap.get(teamId).jobs++;
}
for (const team of teamMap.entries()) {
const [teamId, data] = team;
const user = users.find((u) => u.id === teamId);
const teamName = user ? `${user.first_name || ''} ${user.last_name || ''}`.trim() : teamId;
const teamDefectRate = data.jobs > 0 ? (data.defects / data.jobs) * 100 : 0;
const teamReworkRate = teamDefectRate * 0.6;
const teamQualityScore = 100 - teamDefectRate;
const performanceRating = teamQualityScore >= 95 ? 'Excellent' :
teamQualityScore >= 85 ? 'Good' :
teamQualityScore >= 70 ? 'Needs Improvement' : 'Critical';
qualityByTeams.push({
team_name: teamName,
projects_completed: data.jobs,
defect_rate: teamDefectRate,
rework_rate: teamReworkRate,
customer_satisfaction: customerSatisfaction,
quality_score: teamQualityScore,
performance_rating: performanceRating,
training_recommendations: performanceRating !== 'Excellent' ? ['Quality standards workshop', 'Mentorship program'] : [],
best_practices: performanceRating === 'Excellent' ? ['Standardized processes', 'Peer reviews'] : [],
});
}
qualityByTeams.sort((a, b) => b.quality_score - a.quality_score);
}
// Compliance metrics
const complianceMetrics = [
{
compliance_area: 'Safety Standards',
compliance_rate: 96,
violations_count: 2,
severity_level: 'Low',
corrective_actions: ['Additional safety training', 'Updated safety protocols'],
audit_status: 'Passed',
next_audit_date: new Date(now + (90 * 24 * 60 * 60 * 1000)).toISOString().slice(0, 10),
},
];
// Quality improvements
const qualityImprovements = [
{
improvement_area: 'Workmanship Training Program',
current_state: '35% of defects from workmanship issues',
target_state: '15% defect rate from workmanship',
expected_benefit: '-20% defect rate, +10% customer satisfaction',
implementation_cost: 15000,
roi_estimate: 3.5,
timeline_months: 6,
success_metrics: ['Defect rate reduction', 'Training completion rate', 'Customer satisfaction'],
priority: 1,
},
];
// Pareto analysis
const paretoProblems = [];
if (includeParetoAnalysis) {
let cumulativePercentage = 0;
for (const defect of defectAnalyses) {
cumulativePercentage += defect.defect_percentage;
paretoProblems.push({
problem_category: defect.defect_category,
frequency: defect.total_defects,
cumulative_percentage: cumulativePercentage,
cost_impact: defect.cost_impact,
resolution_complexity: defect.severity === 'Critical' ? 'High' : defect.severity === 'Major' ? 'Medium' : 'Low',
quick_wins_available: defect.severity === 'Minor' && defect.defect_percentage < 20,
recommended_actions: defect.prevention_strategies,
});
}
}
return {
data_source: 'Live JobNimbus API data',
analysis_timestamp: new Date().toISOString(),
time_window_days: timeWindowDays,
quality_metrics: qualityMetrics,
defect_analysis: defectAnalyses,
inspection_metrics: inspectionMetrics,
rework_analysis: reworkAnalyses,
quality_trends: qualityTrends.slice(-12),
customer_satisfaction_correlations: satisfactionCorrelations,
quality_by_team: includeTeamAnalysis ? qualityByTeams.slice(0, 10) : undefined,
compliance_metrics: complianceMetrics,
quality_improvement_recommendations: qualityImprovements,
pareto_analysis: includeParetoAnalysis ? paretoProblems : undefined,
key_insights: [
`Quality score: ${qualityScore.toFixed(0)}/100`,
`Defect rate: ${defectRate.toFixed(1)}%`,
`First time right: ${firstTimeRight.toFixed(1)}%`,
`Six Sigma level: ${sigmaLevel.toFixed(1)}σ`,
],
};
}
catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 'Failed',
};
}
}
}
//# sourceMappingURL=getQualityControlAnalytics.js.map