feat: enhance team overview and skills tabs with improved skill distribution and UI
- Updated team overview tab to calculate and display skill distribution by individual skill levels. - Refactored skill tab layout for better responsiveness and readability, including adjustments to grid and text sizes. - Added a new utility function to calculate skill level distribution for cleaner code and reusability. - Improved visual elements for better user interaction and clarity in skill metrics.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Star, BarChart3, Target } from "lucide-react";
|
||||
import { BarChart3, Target, Star } from "lucide-react";
|
||||
import { TeamStats } from "@/services/admin-service";
|
||||
import { TechIcon } from "@/components/icons/tech-icon";
|
||||
|
||||
@@ -125,48 +125,44 @@ export function TeamOverviewTab({
|
||||
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5 text-blue-400" />
|
||||
Répartition des niveaux
|
||||
Répartition des niveaux par compétence
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{(() => {
|
||||
// Calculer la répartition par compétence individuelle
|
||||
const allSkills = team.members.flatMap((m) => m.skills);
|
||||
const totalSkills = allSkills.length;
|
||||
|
||||
const levelDistribution = {
|
||||
expert: allSkills.filter((s) => s.level === 3).length,
|
||||
autonomous: allSkills.filter((s) => s.level === 2).length,
|
||||
notAutonomous: allSkills.filter((s) => s.level === 1).length,
|
||||
never: allSkills.filter((s) => s.level === 0).length,
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
label: "Expert",
|
||||
count: team.members.filter(
|
||||
(m) =>
|
||||
m.skills.reduce((avg, s) => avg + s.level, 0) /
|
||||
m.skills.length >=
|
||||
2.5
|
||||
).length,
|
||||
count: levelDistribution.expert,
|
||||
total: totalSkills,
|
||||
color: "bg-green-500",
|
||||
},
|
||||
{
|
||||
label: "Avancé",
|
||||
count: team.members.filter((m) => {
|
||||
const avg =
|
||||
m.skills.reduce((avg, s) => avg + s.level, 0) /
|
||||
m.skills.length;
|
||||
return avg >= 2 && avg < 2.5;
|
||||
}).length,
|
||||
label: "Autonome",
|
||||
count: levelDistribution.autonomous,
|
||||
total: totalSkills,
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
{
|
||||
label: "Intermédiaire",
|
||||
count: team.members.filter((m) => {
|
||||
const avg =
|
||||
m.skills.reduce((avg, s) => avg + s.level, 0) /
|
||||
m.skills.length;
|
||||
return avg >= 1 && avg < 2;
|
||||
}).length,
|
||||
label: "Pas autonome",
|
||||
count: levelDistribution.notAutonomous,
|
||||
total: totalSkills,
|
||||
color: "bg-orange-500",
|
||||
},
|
||||
{
|
||||
label: "Débutant",
|
||||
count: team.members.filter(
|
||||
(m) =>
|
||||
m.skills.reduce((avg, s) => avg + s.level, 0) /
|
||||
m.skills.length <
|
||||
1
|
||||
).length,
|
||||
label: "Jamais utilisé",
|
||||
count: levelDistribution.never,
|
||||
total: totalSkills,
|
||||
color: "bg-red-500",
|
||||
},
|
||||
].map((level, idx) => (
|
||||
@@ -176,7 +172,9 @@ export function TeamOverviewTab({
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-3 h-3 rounded-full ${level.color}`} />
|
||||
<span className="text-white font-medium">{level.label}</span>
|
||||
<span className="text-white font-medium">
|
||||
{level.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-white font-bold">{level.count}</span>
|
||||
@@ -184,16 +182,17 @@ export function TeamOverviewTab({
|
||||
<div
|
||||
className={`h-2 rounded-full ${level.color}`}
|
||||
style={{
|
||||
width: `${(level.count / team.totalMembers) * 100}%`,
|
||||
width: `${(level.count / level.total) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 w-10 text-right">
|
||||
{((level.count / team.totalMembers) * 100).toFixed(0)}%
|
||||
{((level.count / level.total) * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -221,16 +220,6 @@ export function TeamOverviewTab({
|
||||
{teamInsights.totalLearners}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<span className="text-slate-300">Mentors disponibles</span>
|
||||
<span className="text-white font-bold">
|
||||
{skillAnalysis.reduce(
|
||||
(sum, skill) =>
|
||||
sum + skill.experts.filter((e) => e.canMentor).length,
|
||||
0
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,19 +95,21 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
|
||||
</div>
|
||||
|
||||
{/* Liste des compétences détaillée */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{filteredSkills.map((skill, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white/5 border border-white/10 rounded-xl p-4 hover:bg-white/10 transition-colors"
|
||||
className="bg-white/5 border border-white/10 rounded-lg p-3 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-white">{skill.skillName}</h4>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-medium text-white text-sm truncate">
|
||||
{skill.skillName}
|
||||
</h4>
|
||||
<p className="text-xs text-slate-400">{skill.category}</p>
|
||||
</div>
|
||||
<div
|
||||
className={`px-2 py-1 border rounded-lg text-center ${getSkillLevelBadgeClasses(
|
||||
className={`px-2 py-1 border rounded text-center ml-2 flex-shrink-0 ${getSkillLevelBadgeClasses(
|
||||
skill.averageLevel
|
||||
)}`}
|
||||
>
|
||||
@@ -117,17 +119,17 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-300">Niveau moyen:</span>
|
||||
<span className="text-white font-semibold">
|
||||
<span className="text-xs text-slate-300">Niveau:</span>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{skill.averageLevel.toFixed(1)}/3
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-white/10 rounded-full h-2">
|
||||
<div className="w-full bg-white/10 rounded-full h-1.5">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${getSkillLevelColor(
|
||||
className={`h-1.5 rounded-full transition-all ${getSkillLevelColor(
|
||||
skill.averageLevel
|
||||
)
|
||||
.replace("bg-", "bg-gradient-to-r from-")
|
||||
@@ -137,32 +139,30 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||
<div className="text-center">
|
||||
<div className="text-white font-semibold">
|
||||
{skill.totalEvaluations}
|
||||
</div>
|
||||
<div className="text-slate-400">Évaluations</div>
|
||||
<div className="text-slate-400 text-xs">Éval.</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-green-300 font-semibold">
|
||||
{skill.expertCount}
|
||||
</div>
|
||||
<div className="text-slate-400">Experts</div>
|
||||
<div className="text-slate-400 text-xs">Experts</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-blue-300 font-semibold">
|
||||
{skill.learnerCount}
|
||||
</div>
|
||||
<div className="text-slate-400">Apprenants</div>
|
||||
<div className="text-slate-400 text-xs">Appren.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{skill.experts.filter((e) => e.canMentor).length > 0 && (
|
||||
<div className="pt-2 border-t border-white/10">
|
||||
<div className="text-xs text-slate-400 mb-1">
|
||||
Mentors disponibles:
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mb-1">Mentors:</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{skill.experts
|
||||
.filter((e) => e.canMentor)
|
||||
@@ -171,15 +171,15 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
|
||||
<Badge
|
||||
key={i}
|
||||
variant="outline"
|
||||
className="text-xs text-green-300 border-green-500/30"
|
||||
className="text-xs text-green-300 border-green-500/30 px-1 py-0.5"
|
||||
>
|
||||
{expert.name}
|
||||
{expert.name.split(" ")[0]}
|
||||
</Badge>
|
||||
))}
|
||||
{skill.experts.filter((e) => e.canMentor).length > 2 && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs text-slate-400 border-slate-500/30"
|
||||
className="text-xs text-slate-400 border-slate-500/30 px-1 py-0.5"
|
||||
>
|
||||
+{skill.experts.filter((e) => e.canMentor).length - 2}
|
||||
</Badge>
|
||||
|
||||
@@ -49,3 +49,20 @@ export function getSkillLevelLabel(level: string): string {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la répartition des niveaux par compétence individuelle
|
||||
*/
|
||||
export function calculateSkillLevelDistribution(
|
||||
members: Array<{ skills: Array<{ level: number }> }>
|
||||
) {
|
||||
const allSkills = members.flatMap((m) => m.skills);
|
||||
|
||||
return {
|
||||
expert: allSkills.filter((s) => s.level === 3).length,
|
||||
autonomous: allSkills.filter((s) => s.level === 2).length,
|
||||
notAutonomous: allSkills.filter((s) => s.level === 1).length,
|
||||
never: allSkills.filter((s) => s.level === 0).length,
|
||||
total: allSkills.length,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user