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:
Julien Froidefond
2025-08-22 13:59:51 +02:00
parent 8974a9b579
commit 5c76ec0549
3 changed files with 103 additions and 97 deletions

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Star, BarChart3, Target } from "lucide-react"; import { BarChart3, Target, Star } from "lucide-react";
import { TeamStats } from "@/services/admin-service"; import { TeamStats } from "@/services/admin-service";
import { TechIcon } from "@/components/icons/tech-icon"; import { TechIcon } from "@/components/icons/tech-icon";
@@ -125,75 +125,74 @@ export function TeamOverviewTab({
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-6"> <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"> <h3 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-blue-400" /> <BarChart3 className="h-5 w-5 text-blue-400" />
Répartition des niveaux Répartition des niveaux par compétence
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
{[ {(() => {
{ // Calculer la répartition par compétence individuelle
label: "Expert", const allSkills = team.members.flatMap((m) => m.skills);
count: team.members.filter( const totalSkills = allSkills.length;
(m) =>
m.skills.reduce((avg, s) => avg + s.level, 0) / const levelDistribution = {
m.skills.length >= expert: allSkills.filter((s) => s.level === 3).length,
2.5 autonomous: allSkills.filter((s) => s.level === 2).length,
).length, notAutonomous: allSkills.filter((s) => s.level === 1).length,
color: "bg-green-500", never: allSkills.filter((s) => s.level === 0).length,
}, };
{
label: "Avancé", return [
count: team.members.filter((m) => { {
const avg = label: "Expert",
m.skills.reduce((avg, s) => avg + s.level, 0) / count: levelDistribution.expert,
m.skills.length; total: totalSkills,
return avg >= 2 && avg < 2.5; color: "bg-green-500",
}).length, },
color: "bg-blue-500", {
}, label: "Autonome",
{ count: levelDistribution.autonomous,
label: "Intermédiaire", total: totalSkills,
count: team.members.filter((m) => { color: "bg-blue-500",
const avg = },
m.skills.reduce((avg, s) => avg + s.level, 0) / {
m.skills.length; label: "Pas autonome",
return avg >= 1 && avg < 2; count: levelDistribution.notAutonomous,
}).length, total: totalSkills,
color: "bg-orange-500", color: "bg-orange-500",
}, },
{ {
label: "Débutant", label: "Jamais utilisé",
count: team.members.filter( count: levelDistribution.never,
(m) => total: totalSkills,
m.skills.reduce((avg, s) => avg + s.level, 0) / color: "bg-red-500",
m.skills.length < },
1 ].map((level, idx) => (
).length, <div
color: "bg-red-500", key={idx}
}, className="flex items-center justify-between p-3 bg-white/5 rounded-lg"
].map((level, idx) => ( >
<div <div className="flex items-center gap-3">
key={idx} <div className={`w-3 h-3 rounded-full ${level.color}`} />
className="flex items-center justify-between p-3 bg-white/5 rounded-lg" <span className="text-white font-medium">
> {level.label}
<div className="flex items-center gap-3"> </span>
<div className={`w-3 h-3 rounded-full ${level.color}`} /> </div>
<span className="text-white font-medium">{level.label}</span> <div className="flex items-center gap-3">
</div> <span className="text-white font-bold">{level.count}</span>
<div className="flex items-center gap-3"> <div className="w-16 bg-white/10 rounded-full h-2">
<span className="text-white font-bold">{level.count}</span> <div
<div className="w-16 bg-white/10 rounded-full h-2"> className={`h-2 rounded-full ${level.color}`}
<div style={{
className={`h-2 rounded-full ${level.color}`} width: `${(level.count / level.total) * 100}%`,
style={{ }}
width: `${(level.count / team.totalMembers) * 100}%`, />
}} </div>
/> <span className="text-xs text-slate-400 w-10 text-right">
{((level.count / level.total) * 100).toFixed(0)}%
</span>
</div> </div>
<span className="text-xs text-slate-400 w-10 text-right">
{((level.count / team.totalMembers) * 100).toFixed(0)}%
</span>
</div> </div>
</div> ));
))} })()}
</div> </div>
</div> </div>
@@ -221,16 +220,6 @@ export function TeamOverviewTab({
{teamInsights.totalLearners} {teamInsights.totalLearners}
</span> </span>
</div> </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> </div>
</div> </div>

View File

@@ -95,19 +95,21 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
</div> </div>
{/* Liste des compétences détaillée */} {/* 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) => ( {filteredSkills.map((skill, idx) => (
<div <div
key={idx} 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 className="flex items-center justify-between mb-2">
<div> <div className="min-w-0 flex-1">
<h4 className="font-medium text-white">{skill.skillName}</h4> <h4 className="font-medium text-white text-sm truncate">
{skill.skillName}
</h4>
<p className="text-xs text-slate-400">{skill.category}</p> <p className="text-xs text-slate-400">{skill.category}</p>
</div> </div>
<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 skill.averageLevel
)}`} )}`}
> >
@@ -117,17 +119,17 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
</div> </div>
</div> </div>
<div className="space-y-3"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-slate-300">Niveau moyen:</span> <span className="text-xs text-slate-300">Niveau:</span>
<span className="text-white font-semibold"> <span className="text-white font-semibold text-sm">
{skill.averageLevel.toFixed(1)}/3 {skill.averageLevel.toFixed(1)}/3
</span> </span>
</div> </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 <div
className={`h-2 rounded-full transition-all ${getSkillLevelColor( className={`h-1.5 rounded-full transition-all ${getSkillLevelColor(
skill.averageLevel skill.averageLevel
) )
.replace("bg-", "bg-gradient-to-r from-") .replace("bg-", "bg-gradient-to-r from-")
@@ -137,32 +139,30 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
/> />
</div> </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-center">
<div className="text-white font-semibold"> <div className="text-white font-semibold">
{skill.totalEvaluations} {skill.totalEvaluations}
</div> </div>
<div className="text-slate-400">Évaluations</div> <div className="text-slate-400 text-xs">Éval.</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-green-300 font-semibold"> <div className="text-green-300 font-semibold">
{skill.expertCount} {skill.expertCount}
</div> </div>
<div className="text-slate-400">Experts</div> <div className="text-slate-400 text-xs">Experts</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-blue-300 font-semibold"> <div className="text-blue-300 font-semibold">
{skill.learnerCount} {skill.learnerCount}
</div> </div>
<div className="text-slate-400">Apprenants</div> <div className="text-slate-400 text-xs">Appren.</div>
</div> </div>
</div> </div>
{skill.experts.filter((e) => e.canMentor).length > 0 && ( {skill.experts.filter((e) => e.canMentor).length > 0 && (
<div className="pt-2 border-t border-white/10"> <div className="pt-2 border-t border-white/10">
<div className="text-xs text-slate-400 mb-1"> <div className="text-xs text-slate-400 mb-1">Mentors:</div>
Mentors disponibles:
</div>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{skill.experts {skill.experts
.filter((e) => e.canMentor) .filter((e) => e.canMentor)
@@ -171,15 +171,15 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
<Badge <Badge
key={i} key={i}
variant="outline" 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> </Badge>
))} ))}
{skill.experts.filter((e) => e.canMentor).length > 2 && ( {skill.experts.filter((e) => e.canMentor).length > 2 && (
<Badge <Badge
variant="outline" 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} +{skill.experts.filter((e) => e.canMentor).length - 2}
</Badge> </Badge>

View File

@@ -49,3 +49,20 @@ export function getSkillLevelLabel(level: string): string {
return ""; 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,
};
}