Files
peakskills/components/team-review/team-stats.tsx
2025-08-27 12:56:48 +02:00

430 lines
15 KiB
TypeScript

"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TeamReviewData } from "@/lib/team-review-types";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
Legend,
} from "recharts";
import {
Tooltip as UITooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface TeamStatsProps {
stats: TeamReviewData["stats"];
members: TeamReviewData["members"];
skillGaps: TeamReviewData["skillGaps"];
categoryCoverage: TeamReviewData["categoryCoverage"];
}
export function TeamStats({
stats,
members,
skillGaps,
categoryCoverage,
}: TeamStatsProps) {
// Calcul des statistiques avancées
const totalEvaluations = members.reduce(
(acc, member) => acc + member.skills.length,
0
);
const avgSkillsPerMember = totalEvaluations / (stats.totalMembers || 1);
// Distribution des niveaux
const levelDistribution = members.reduce((acc, member) => {
member.skills.forEach((skill) => {
acc[skill.level] = (acc[skill.level] || 0) + 1;
});
return acc;
}, {} as Record<string, number>);
const levelData = [
{ name: "Expert", value: levelDistribution.expert || 0, color: "#4f46e5" },
{
name: "Autonome",
value: levelDistribution.autonomous || 0,
color: "#10b981",
},
{
name: "En apprentissage",
value: levelDistribution["not-autonomous"] || 0,
color: "#f59e0b",
},
{
name: "Jamais pratiqué",
value: levelDistribution.never || 0,
color: "#6b7280",
},
];
// Top catégories par expertise
const sortedCategories = [...categoryCoverage].sort(
(a, b) => b.experts - a.experts
);
const topCategories = sortedCategories.slice(0, 3);
// Statistiques de mentorat
const mentorCount = members.filter((m) => m.mentorSkills > 0).length;
const learnerCount = members.filter((m) => m.learningSkills > 0).length;
const mentorshipRatio = (mentorCount / (stats.totalMembers || 1)) * 100;
// Gaps critiques par catégorie, séparés par importance
const criticalGapsByCategory = skillGaps.reduce((acc, gap) => {
const isIncontournableUndercovered =
gap.importance === "incontournable" && gap.coverage < 75;
const isMajeureUndercovered =
gap.importance === "majeure" && gap.coverage < 60;
if (isIncontournableUndercovered || isMajeureUndercovered) {
if (!acc[gap.category]) {
acc[gap.category] = { incontournable: 0, majeure: 0 };
}
if (isIncontournableUndercovered) {
acc[gap.category].incontournable++;
}
if (isMajeureUndercovered) {
acc[gap.category].majeure++;
}
}
return acc;
}, {} as Record<string, { incontournable: number; majeure: number }>);
const gapData = Object.entries(criticalGapsByCategory)
.map(([category, counts]) => ({
name: category,
incontournable: counts.incontournable,
majeure: counts.majeure,
}))
.sort(
(a, b) =>
// Trier d'abord par nombre total de compétences critiques
b.incontournable + b.majeure - (a.incontournable + a.majeure) ||
// Puis par nombre de compétences incontournables
b.incontournable - a.incontournable
);
// Nombre total de compétences critiques sous-couvertes
const totalCriticalGaps = Object.values(criticalGapsByCategory).reduce(
(sum, counts) => sum + counts.incontournable + counts.majeure,
0
);
return (
<Card className="bg-white/5 border-white/10 backdrop-blur">
<CardHeader>
<CardTitle className="text-slate-200">
Statistiques de l'équipe
</CardTitle>
</CardHeader>
<CardContent className="space-y-8">
{/* Vue d'ensemble */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="space-y-1">
<p className="text-sm text-slate-400">Membres</p>
<p className="text-2xl font-bold text-slate-200">
{stats.totalMembers}
</p>
</div>
<div className="space-y-1">
<p className="text-sm text-slate-400">Compétences/membre</p>
<p className="text-2xl font-bold text-slate-200">
{avgSkillsPerMember.toFixed(1)}
</p>
</div>
<div className="space-y-1">
<p className="text-sm text-slate-400">Ratio mentors</p>
<p className="text-2xl font-bold text-slate-200">
{mentorshipRatio.toFixed(0)}%
</p>
</div>
<UITooltip>
<TooltipTrigger asChild>
<div className="space-y-1">
<p className="text-sm text-slate-400">Compétences critiques</p>
<p className="text-2xl font-bold text-red-400">
{totalCriticalGaps}
</p>
</div>
</TooltipTrigger>
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
<div className="text-xs space-y-1">
<p>Compétences critiques sous-couvertes :</p>
<ul className="list-disc list-inside space-y-0.5">
<li>Incontournables : couverture &lt; 75%</li>
<li>Majeures : couverture &lt; 60%</li>
</ul>
</div>
</TooltipContent>
</UITooltip>
</div>
{/* Couverture des compétences critiques */}
<div>
<h3 className="text-sm font-medium text-slate-200 mb-4">
Couverture des compétences critiques
</h3>
<div className="grid grid-cols-2 gap-4">
<UITooltip>
<TooltipTrigger asChild>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-slate-400">
Incontournables
</span>
<span className="text-sm font-medium text-slate-200">
{stats.criticalSkillsCoverage.incontournable.toFixed(0)}%
</span>
</div>
<div className="h-2 w-full bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-red-500/50 rounded-full transition-all"
style={{
width: `${Math.min(
100,
Math.max(
0,
stats.criticalSkillsCoverage.incontournable
)
)}%`,
}}
/>
</div>
</div>
</TooltipTrigger>
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
<div className="text-xs">
<p>Compétences incontournables</p>
<p className="text-slate-400">Objectif : 75% de couverture</p>
</div>
</TooltipContent>
</UITooltip>
<UITooltip>
<TooltipTrigger asChild>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-slate-400">Majeures</span>
<span className="text-sm font-medium text-slate-200">
{stats.criticalSkillsCoverage.majeure.toFixed(0)}%
</span>
</div>
<div className="h-2 w-full bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-orange-500/50 rounded-full transition-all"
style={{
width: `${Math.min(
100,
Math.max(0, stats.criticalSkillsCoverage.majeure)
)}%`,
}}
/>
</div>
</div>
</TooltipTrigger>
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
<div className="text-xs">
<p>Compétences majeures</p>
<p className="text-slate-400">Objectif : 60% de couverture</p>
</div>
</TooltipContent>
</UITooltip>
</div>
</div>
{/* Distribution des niveaux */}
<div>
<h3 className="text-sm font-medium text-slate-200 mb-4">
Distribution des niveaux
</h3>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={levelData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={80}
label={(entry) => `${entry.name} (${entry.value})`}
labelLine={{ stroke: "#64748b" }}
>
{levelData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
className="opacity-80"
/>
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: "rgba(30, 41, 59, 0.9)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
}}
itemStyle={{ color: "#e2e8f0" }}
labelStyle={{ color: "#e2e8f0" }}
/>
</PieChart>
</ResponsiveContainer>
</div>
</div>
{/* Top catégories */}
<div>
<h3 className="text-sm font-medium text-slate-200 mb-4">
Top catégories par expertise
</h3>
<div className="space-y-3">
{topCategories.map((cat) => (
<div key={cat.category} className="flex items-center gap-4">
<div className="flex-1">
<p className="text-sm font-medium text-slate-300">
{cat.category}
</p>
<div className="flex gap-2 text-sm text-slate-400">
<span>{cat.experts} experts</span>
<span></span>
<span>{cat.mentors} mentors</span>
</div>
</div>
<div className="w-24 text-right">
<p className="text-sm font-medium text-slate-300">
{cat.coverage.toFixed(0)}%
</p>
<p className="text-sm text-slate-400">couverture</p>
</div>
</div>
))}
</div>
</div>
{/* Gaps critiques par catégorie */}
{gapData.length > 0 && (
<div>
<h3 className="text-sm font-medium text-slate-200 mb-4">
Compétences critiques sous-couvertes par catégorie
</h3>
<div className="h-[250px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={gapData}>
<CartesianGrid
strokeDasharray="3 3"
stroke="rgba(255, 255, 255, 0.1)"
/>
<XAxis
dataKey="name"
tick={{ fill: "#94a3b8" }}
axisLine={{ stroke: "#334155" }}
/>
<YAxis
tick={{ fill: "#94a3b8" }}
axisLine={{ stroke: "#334155" }}
/>
<Tooltip
contentStyle={{
backgroundColor: "rgba(30, 41, 59, 0.9)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
}}
itemStyle={{ color: "#e2e8f0" }}
labelStyle={{ color: "#e2e8f0" }}
formatter={(value, name) => {
const label =
name === "incontournable"
? "Incontournables (obj. 75%)"
: "Majeures (obj. 60%)";
return [value, label];
}}
/>
<Legend
formatter={(value) => {
return value === "incontournable"
? "Incontournables (obj. 75%)"
: "Majeures (obj. 60%)";
}}
wrapperStyle={{
paddingTop: "20px",
}}
/>
<Bar
dataKey="incontournable"
fill="#ef4444"
name="incontournable"
stackId="stack"
/>
<Bar
dataKey="majeure"
fill="#f97316"
name="majeure"
stackId="stack"
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Statistiques de mentorat */}
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-white/10">
<div>
<h3 className="text-sm font-medium text-slate-200 mb-2">
Mentorat
</h3>
<div className="space-y-1">
<div className="flex justify-between">
<p className="text-sm text-slate-400">Mentors</p>
<p className="text-sm font-medium text-slate-300">
{mentorCount}
</p>
</div>
<div className="flex justify-between">
<p className="text-sm text-slate-400">Apprenants</p>
<p className="text-sm font-medium text-slate-300">
{learnerCount}
</p>
</div>
<div className="flex justify-between">
<p className="text-sm text-slate-400">Opportunités</p>
<p className="text-sm font-medium text-slate-300">
{stats.mentorshipOpportunities}
</p>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-slate-200 mb-2">
Couverture globale
</h3>
<div className="space-y-1">
<div className="flex justify-between">
<p className="text-sm text-slate-400">Compétences</p>
<p className="text-sm font-medium text-slate-300">
{stats.totalSkills}
</p>
</div>
<div className="flex justify-between">
<p className="text-sm text-slate-400">Niveau moyen</p>
<p className="text-sm font-medium text-slate-300">
{((stats.averageTeamLevel * 100) / 3).toFixed(0)}%
</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
}