feat: my team page
This commit is contained in:
285
components/team-review/team-stats.tsx
Normal file
285
components/team-review/team-stats.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
"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,
|
||||
} from "recharts";
|
||||
|
||||
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
|
||||
const criticalGapsByCategory = skillGaps
|
||||
.filter((gap) => gap.risk === "high")
|
||||
.reduce((acc, gap) => {
|
||||
acc[gap.category] = (acc[gap.category] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const gapData = Object.entries(criticalGapsByCategory).map(
|
||||
([category, count]) => ({
|
||||
name: category,
|
||||
value: count,
|
||||
fill: "#ef4444",
|
||||
})
|
||||
);
|
||||
|
||||
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>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-slate-400">Gaps critiques</p>
|
||||
<p className="text-2xl font-bold text-red-400">
|
||||
{stats.learningNeeds}
|
||||
</p>
|
||||
</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">
|
||||
Gaps critiques par catégorie
|
||||
</h3>
|
||||
<div className="h-[200px]">
|
||||
<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" }}
|
||||
/>
|
||||
<Bar dataKey="value" />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user