- Refined color schemes in `BurndownChart`, `CollaborationMatrix`, `ThroughputChart`, and `TeamActivityHeatmap` for better visibility and consistency. - Adjusted opacity handling in `TeamActivityHeatmap` for improved visual clarity. - Cleaned up imports in `CollaborationMatrix` and `SprintComparison` for better code organization. - Enhanced `JiraSync` component with updated color for the sync status indicator. - Updated `jira-period-filter` to remove unused imports, streamlining the codebase.
337 lines
13 KiB
TypeScript
337 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
|
|
import { SprintVelocity } from '@/lib/types';
|
|
import { Card } from '@/components/ui/Card';
|
|
|
|
interface SprintComparisonProps {
|
|
sprintHistory: SprintVelocity[];
|
|
className?: string;
|
|
}
|
|
|
|
interface ComparisonMetrics {
|
|
velocityTrend: 'improving' | 'declining' | 'stable';
|
|
avgCompletion: number;
|
|
consistency: 'high' | 'medium' | 'low';
|
|
bestSprint: SprintVelocity;
|
|
worstSprint: SprintVelocity;
|
|
predictions: {
|
|
nextSprintEstimate: number;
|
|
confidenceLevel: 'high' | 'medium' | 'low';
|
|
};
|
|
}
|
|
|
|
export function SprintComparison({ sprintHistory, className }: SprintComparisonProps) {
|
|
// Analyser les tendances
|
|
const metrics = analyzeSprintTrends(sprintHistory);
|
|
|
|
// Données pour les graphiques
|
|
const comparisonData = sprintHistory.map(sprint => ({
|
|
name: sprint.sprintName.replace('Sprint ', ''),
|
|
completion: sprint.completionRate,
|
|
velocity: sprint.completedPoints,
|
|
planned: sprint.plannedPoints,
|
|
variance: sprint.plannedPoints > 0
|
|
? ((sprint.completedPoints - sprint.plannedPoints) / sprint.plannedPoints) * 100
|
|
: 0
|
|
}));
|
|
|
|
const CustomTooltip = ({ active, payload, label }: {
|
|
active?: boolean;
|
|
payload?: Array<{ value: number; name: string; color: string }>;
|
|
label?: string
|
|
}) => {
|
|
if (active && payload && payload.length) {
|
|
return (
|
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
|
|
<p className="font-medium text-sm mb-2">Sprint {label}</p>
|
|
<div className="space-y-1 text-xs">
|
|
{payload.map((item, index) => (
|
|
<div key={index} className="flex justify-between gap-4">
|
|
<span style={{ color: item.color }}>{item.name}:</span>
|
|
<span className="font-mono" style={{ color: item.color }}>
|
|
{typeof item.value === 'number' ?
|
|
(item.name.includes('%') ? `${item.value}%` : item.value) :
|
|
item.value
|
|
}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<div className={className}>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Graphique de comparaison des taux de complétion */}
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-3">Évolution des taux de complétion</h4>
|
|
<div style={{ width: '100%', height: '200px' }}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart data={comparisonData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
|
<XAxis
|
|
dataKey="name"
|
|
stroke="var(--muted-foreground)"
|
|
fontSize={10}
|
|
/>
|
|
<YAxis
|
|
stroke="var(--muted-foreground)"
|
|
fontSize={10}
|
|
domain={[0, 100]}
|
|
label={{ value: '%', angle: 0, position: 'insideLeft' }}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="completion"
|
|
stroke="hsl(217, 91%, 60%)"
|
|
strokeWidth={3}
|
|
dot={{ fill: 'hsl(217, 91%, 60%)', strokeWidth: 2, r: 5 }}
|
|
name="Taux de complétion"
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Graphique de comparaison des vélocités */}
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-3">Comparaison planifié vs réalisé</h4>
|
|
<div style={{ width: '100%', height: '200px' }}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={comparisonData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
|
<XAxis
|
|
dataKey="name"
|
|
stroke="var(--muted-foreground)"
|
|
fontSize={10}
|
|
/>
|
|
<YAxis
|
|
stroke="var(--muted-foreground)"
|
|
fontSize={10}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Bar dataKey="planned" name="Planifié" fill="hsl(240, 5%, 64%)" radius={[2, 2, 0, 0]} />
|
|
<Bar dataKey="velocity" name="Réalisé" fill="hsl(217, 91%, 60%)" radius={[2, 2, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Métriques de comparaison */}
|
|
<div className="mt-6 grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Card className="p-3">
|
|
<div className="text-center">
|
|
<div className={`text-lg font-bold ${
|
|
metrics.velocityTrend === 'improving' ? 'text-green-500' :
|
|
metrics.velocityTrend === 'declining' ? 'text-red-500' : 'text-blue-500'
|
|
}`}>
|
|
{metrics.velocityTrend === 'improving' ? '📈' :
|
|
metrics.velocityTrend === 'declining' ? '📉' : '➡️'}
|
|
</div>
|
|
<div className="text-xs text-[var(--muted-foreground)] mt-1">
|
|
Tendance générale
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-3">
|
|
<div className="text-center">
|
|
<div className={`text-lg font-bold ${
|
|
metrics.avgCompletion > 80 ? 'text-green-500' :
|
|
metrics.avgCompletion > 60 ? 'text-orange-500' : 'text-red-500'
|
|
}`}>
|
|
{Math.round(metrics.avgCompletion)}%
|
|
</div>
|
|
<div className="text-xs text-[var(--muted-foreground)] mt-1">
|
|
Complétion moyenne
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-3">
|
|
<div className="text-center">
|
|
<div className={`text-lg font-bold ${
|
|
metrics.consistency === 'high' ? 'text-green-500' :
|
|
metrics.consistency === 'medium' ? 'text-orange-500' : 'text-red-500'
|
|
}`}>
|
|
{metrics.consistency === 'high' ? 'Haute' :
|
|
metrics.consistency === 'medium' ? 'Moyenne' : 'Faible'}
|
|
</div>
|
|
<div className="text-xs text-[var(--muted-foreground)] mt-1">
|
|
Consistance
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-3">
|
|
<div className="text-center">
|
|
<div className={`text-lg font-bold ${
|
|
metrics.predictions.confidenceLevel === 'high' ? 'text-green-500' :
|
|
metrics.predictions.confidenceLevel === 'medium' ? 'text-orange-500' : 'text-red-500'
|
|
}`}>
|
|
{metrics.predictions.nextSprintEstimate}
|
|
</div>
|
|
<div className="text-xs text-[var(--muted-foreground)] mt-1">
|
|
Prédiction suivante
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Insights et recommandations */}
|
|
<div className="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<Card className="p-4">
|
|
<h4 className="text-sm font-medium mb-3">🏆 Meilleur sprint</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span>Sprint:</span>
|
|
<span className="font-semibold">{metrics.bestSprint.sprintName}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Points complétés:</span>
|
|
<span className="font-semibold text-green-500">{metrics.bestSprint.completedPoints}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Taux de complétion:</span>
|
|
<span className="font-semibold text-green-500">{metrics.bestSprint.completionRate}%</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4">
|
|
<h4 className="text-sm font-medium mb-3">📉 Sprint à améliorer</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span>Sprint:</span>
|
|
<span className="font-semibold">{metrics.worstSprint.sprintName}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Points complétés:</span>
|
|
<span className="font-semibold text-red-500">{metrics.worstSprint.completedPoints}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Taux de complétion:</span>
|
|
<span className="font-semibold text-red-500">{metrics.worstSprint.completionRate}%</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Recommandations */}
|
|
<Card className="mt-4 p-4">
|
|
<h4 className="text-sm font-medium mb-2">💡 Recommandations</h4>
|
|
<div className="space-y-2 text-sm">
|
|
{getRecommendations(metrics).map((recommendation, index) => (
|
|
<div key={index} className="flex items-start gap-2">
|
|
<span className="text-blue-500 mt-0.5">•</span>
|
|
<span>{recommendation}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Analyse les tendances des sprints
|
|
*/
|
|
function analyzeSprintTrends(sprintHistory: SprintVelocity[]): ComparisonMetrics {
|
|
if (sprintHistory.length === 0) {
|
|
return {
|
|
velocityTrend: 'stable',
|
|
avgCompletion: 0,
|
|
consistency: 'low',
|
|
bestSprint: sprintHistory[0],
|
|
worstSprint: sprintHistory[0],
|
|
predictions: { nextSprintEstimate: 0, confidenceLevel: 'low' }
|
|
};
|
|
}
|
|
|
|
// Tendance de vélocité (comparer premiers vs derniers sprints)
|
|
const firstHalf = sprintHistory.slice(0, Math.ceil(sprintHistory.length / 2));
|
|
const secondHalf = sprintHistory.slice(Math.floor(sprintHistory.length / 2));
|
|
|
|
const firstHalfAvg = firstHalf.reduce((sum, s) => sum + s.completedPoints, 0) / firstHalf.length;
|
|
const secondHalfAvg = secondHalf.reduce((sum, s) => sum + s.completedPoints, 0) / secondHalf.length;
|
|
|
|
const improvementRate = (secondHalfAvg - firstHalfAvg) / firstHalfAvg * 100;
|
|
const velocityTrend: 'improving' | 'declining' | 'stable' =
|
|
improvementRate > 10 ? 'improving' :
|
|
improvementRate < -10 ? 'declining' : 'stable';
|
|
|
|
// Complétion moyenne
|
|
const avgCompletion = sprintHistory.reduce((sum, s) => sum + s.completionRate, 0) / sprintHistory.length;
|
|
|
|
// Consistance (variance des taux de complétion)
|
|
const completionRates = sprintHistory.map(s => s.completionRate);
|
|
const variance = completionRates.reduce((sum, rate) => sum + Math.pow(rate - avgCompletion, 2), 0) / completionRates.length;
|
|
const standardDeviation = Math.sqrt(variance);
|
|
|
|
const consistency: 'high' | 'medium' | 'low' =
|
|
standardDeviation < 10 ? 'high' :
|
|
standardDeviation < 20 ? 'medium' : 'low';
|
|
|
|
// Meilleur et pire sprint
|
|
const bestSprint = sprintHistory.reduce((best, current) =>
|
|
current.completionRate > best.completionRate ? current : best);
|
|
const worstSprint = sprintHistory.reduce((worst, current) =>
|
|
current.completionRate < worst.completionRate ? current : worst);
|
|
|
|
// Prédiction pour le prochain sprint
|
|
const recentSprints = sprintHistory.slice(-3); // 3 derniers sprints
|
|
const recentAvg = recentSprints.reduce((sum, s) => sum + s.completedPoints, 0) / recentSprints.length;
|
|
const nextSprintEstimate = Math.round(recentAvg);
|
|
|
|
const confidenceLevel: 'high' | 'medium' | 'low' =
|
|
consistency === 'high' && velocityTrend !== 'declining' ? 'high' :
|
|
consistency === 'medium' ? 'medium' : 'low';
|
|
|
|
return {
|
|
velocityTrend,
|
|
avgCompletion,
|
|
consistency,
|
|
bestSprint,
|
|
worstSprint,
|
|
predictions: { nextSprintEstimate, confidenceLevel }
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Génère des recommandations basées sur l'analyse
|
|
*/
|
|
function getRecommendations(metrics: ComparisonMetrics): string[] {
|
|
const recommendations: string[] = [];
|
|
|
|
if (metrics.velocityTrend === 'declining') {
|
|
recommendations.push("Tendance en baisse détectée - Identifier les blockers récurrents");
|
|
recommendations.push("Revoir les estimations ou la complexité des tâches récentes");
|
|
} else if (metrics.velocityTrend === 'improving') {
|
|
recommendations.push("Excellente progression ! Maintenir les bonnes pratiques actuelles");
|
|
}
|
|
|
|
if (metrics.avgCompletion < 60) {
|
|
recommendations.push("Taux de complétion faible - Considérer des sprints plus courts ou moins ambitieux");
|
|
} else if (metrics.avgCompletion > 90) {
|
|
recommendations.push("Taux de complétion très élevé - L'équipe pourrait prendre plus d'engagements");
|
|
}
|
|
|
|
if (metrics.consistency === 'low') {
|
|
recommendations.push("Consistance faible - Améliorer la prévisibilité des estimations");
|
|
recommendations.push("Organiser des rétrospectives pour identifier les causes de variabilité");
|
|
}
|
|
|
|
if (recommendations.length === 0) {
|
|
recommendations.push("Performance stable et prévisible - Continuer sur cette lancée !");
|
|
}
|
|
|
|
return recommendations;
|
|
}
|