feat: review my team page with importance

This commit is contained in:
Julien Froidefond
2025-08-27 12:56:48 +02:00
parent 88dc00a44b
commit 94a18b0ca5
8 changed files with 544 additions and 151 deletions

View File

@@ -53,6 +53,7 @@ async function TeamReviewPage() {
stats={teamData.stats}
members={teamData.members}
categoryCoverage={teamData.categoryCoverage}
skillGaps={teamData.skillGaps}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">

View File

@@ -13,6 +13,7 @@ import { Badge } from "@/components/ui/badge";
import { TeamMemberProfile, SkillGap } from "@/lib/team-review-types";
import { UserCheck, GraduationCap } from "lucide-react";
import { TechIcon } from "@/components/icons/tech-icon";
import { getImportanceColors } from "@/lib/tech-colors";
interface SkillMatrixProps {
members: TeamMemberProfile[];
@@ -106,28 +107,32 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
</TableRow>
</TableHeader>
<TableBody>
{skills.map((skill) => (
{skills.map((skill) => {
const colors = getImportanceColors(skill.importance);
return (
<TableRow
key={`skill-row-${skill.skillId}-${skill.category}`}
className="border-white/10"
>
<TableCell className="font-medium">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-white/10 border border-white/20 flex items-center justify-center">
<div
className={`w-8 h-8 rounded-lg ${colors.bg} ${colors.border} border flex items-center justify-center`}
>
<TechIcon
iconName={skill.icon || ""}
className="w-4 h-4 text-blue-400"
className={`w-4 h-4 ${colors.text}`}
fallbackText={skill.skillName}
/>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-200">
<div className="flex flex-col gap-1">
<span className={`${colors.text} font-medium`}>
{skill.skillName}
</span>
{skill.risk === "high" && (
<Badge
variant="destructive"
className="bg-red-500/20 text-red-200 border-red-500/30"
className="bg-red-500/20 text-red-200 border-red-500/30 w-fit"
>
Risque
</Badge>
@@ -171,7 +176,10 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
<div className="flex items-center gap-2">
<div className="w-full bg-white/10 rounded-full h-2">
<div
className="bg-blue-500/50 h-2 rounded-full"
className={`h-2 rounded-full ${colors.bg.replace(
"/20",
"/50"
)}`}
style={{
width: `${Math.max(
0,
@@ -180,13 +188,16 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
}}
/>
</div>
<span className="text-sm text-slate-400 whitespace-nowrap">
<span
className={`text-sm ${colors.text} whitespace-nowrap`}
>
{Math.round(skill.coverage || 0)}%
</span>
</div>
</TableCell>
</TableRow>
))}
);
})}
</TableBody>
</Table>
</div>

View File

@@ -2,6 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { CategoryCoverage } from "@/lib/team-review-types";
import { Progress } from "@/components/ui/progress";
import { AlertTriangle, AlertCircle } from "lucide-react";
interface TeamInsightsProps {
recommendations: string[];
@@ -22,9 +23,27 @@ export function TeamInsights({
<CardContent className="space-y-6">
<div className="space-y-4">
{recommendations.map((recommendation, index) => (
<Alert key={index} className="bg-white/5 border-white/10">
<AlertDescription className="text-slate-300">
{recommendation}
<Alert
key={index}
className={`${
recommendation.includes("⚠️")
? "bg-red-500/10 border-red-500/30"
: "bg-white/5 border-white/10"
}`}
>
<AlertDescription
className={`${
recommendation.includes("⚠️")
? "text-red-200"
: "text-slate-300"
} flex items-start gap-2`}
>
{recommendation.includes("⚠️") ? (
<AlertTriangle className="h-5 w-5 text-red-400 shrink-0" />
) : (
<AlertCircle className="h-5 w-5 text-blue-400 shrink-0" />
)}
<span>{recommendation.replace("⚠️ ", "")}</span>
</AlertDescription>
</Alert>
))}
@@ -79,6 +98,52 @@ export function TeamInsights({
app.
</div>
</div>
{/* Couverture des compétences critiques */}
<div className="grid grid-cols-2 gap-2 pt-2">
<div className="space-y-1">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-400">
Incontournables
</span>
<span className="text-xs font-medium text-red-300">
{category.criticalSkillsCoverage.incontournable}
</span>
</div>
<div className="h-1 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,
category.criticalSkillsCoverage.incontournable
)
)}%`,
}}
/>
</div>
</div>
<div className="space-y-1">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-400">Majeures</span>
<span className="text-xs font-medium text-orange-300">
{category.criticalSkillsCoverage.majeure}
</span>
</div>
<div className="h-1 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, category.criticalSkillsCoverage.majeure)
)}%`,
}}
/>
</div>
</div>
</div>
</div>
))}
</div>

View File

@@ -1,14 +1,23 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TeamReviewData } from "@/lib/team-review-types";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
import { AlertTriangle } from "lucide-react";
import { getImportanceColors } from "@/lib/tech-colors";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface TeamOverviewProps {
team: TeamReviewData["team"];
stats: TeamReviewData["stats"];
members: TeamReviewData["members"];
categoryCoverage: TeamReviewData["categoryCoverage"];
skillGaps: TeamReviewData["skillGaps"];
}
export function TeamOverview({
@@ -16,6 +25,7 @@ export function TeamOverview({
stats,
members,
categoryCoverage,
skillGaps,
}: TeamOverviewProps) {
// Trouver les top contributeurs
const topContributors = [...members]
@@ -29,24 +39,51 @@ export function TeamOverview({
// Trouver les catégories qui nécessitent de l'attention
const categoriesNeedingAttention = [...categoryCoverage]
.filter((cat) => {
// Une catégorie nécessite de l'attention si :
// - Couverture faible (< 40%)
// - OU pas assez d'experts (< 2) avec une équipe de taille significative (> 5)
// - OU beaucoup d'apprenants (> 30% de l'équipe) avec peu de mentors (< 2)
return (
cat.coverage < 40 ||
(cat.experts < 2 && stats.totalMembers > 5) ||
(cat.learners > stats.totalMembers * 0.3 && cat.mentors < 2)
.map((cat) => {
// Pour chaque catégorie, on identifie :
// 1. Les compétences incontournables sous-couvertes (< 75%)
const uncoveredIncontournables = skillGaps.filter(
(gap) =>
gap.category === cat.category &&
gap.importance === "incontournable" &&
gap.coverage < 75
);
// 2. Les compétences majeures sous-couvertes (< 60%)
const uncoveredMajeures = skillGaps.filter(
(gap) =>
gap.category === cat.category &&
gap.importance === "majeure" &&
gap.coverage < 60
);
// Une catégorie nécessite de l'attention si :
const needsAttention =
uncoveredIncontournables.length > 0 || // Il y a des compétences incontournables sous-couvertes
uncoveredMajeures.length > 0 || // OU des compétences majeures sous-couvertes
(cat.experts < 2 && stats.totalMembers > 5); // OU pas assez d'experts dans une équipe significative
if (!needsAttention) return null;
// Calculer un score d'attention pour trier les catégories
const attentionScore =
// Les incontournables pèsent plus lourd
uncoveredIncontournables.length * 3 +
// Les majeures un peu moins
uncoveredMajeures.length * 2 +
// Manque d'experts est un facteur aggravant
(cat.experts < 2 && stats.totalMembers > 5 ? 1 : 0);
return {
...cat,
// On combine les deux types pour l'affichage
criticalSkills: [...uncoveredIncontournables, ...uncoveredMajeures],
attentionScore,
};
})
.sort((a, b) => {
// Prioriser les catégories avec le plus de besoins
const aScore = a.learners * 2 - a.mentors - a.experts;
const bScore = b.learners * 2 - b.mentors - b.experts;
return bScore - aScore;
})
.slice(0, 2);
.filter(Boolean)
.sort((a, b) => b.attentionScore - a.attentionScore)
.slice(0, 3);
return (
<Card className="bg-white/5 border-white/10 backdrop-blur">
@@ -130,23 +167,76 @@ export function TeamOverview({
variant="outline"
className="text-amber-400 border-amber-400/30"
>
{cat.learners} apprenants
{cat.experts} exp {cat.mentors} ment
</Badge>
</div>
</div>
<div className="flex items-center gap-4 text-sm text-slate-400">
<span>{cat.experts} experts</span>
<span>{cat.mentors} mentors</span>
{/* Compétences sous-couvertes */}
{cat.criticalSkills.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{cat.criticalSkills
.sort((a, b) => {
if (
a.importance === "incontournable" &&
b.importance !== "incontournable"
)
return -1;
if (
a.importance !== "incontournable" &&
b.importance === "incontournable"
)
return 1;
return a.coverage - b.coverage;
})
.map((skill) => {
const colors = getImportanceColors(skill.importance);
const target =
skill.importance === "incontournable" ? 75 : 60;
return (
<Tooltip key={skill.skillId}>
<TooltipTrigger>
<div
className={`text-xs rounded-md px-1.5 py-0.5 ${colors.bg} ${colors.border} border flex items-center gap-1.5`}
>
<span className={colors.text}>
{skill.skillName}
</span>
<span
className={
skill.coverage < target
? "text-red-400"
: "text-slate-400"
}
>
{skill.coverage.toFixed(0)}%
</span>
</div>
<div className="text-xs text-amber-400/80">
{cat.coverage < 40 && "Couverture insuffisante • "}
{cat.experts < 2 &&
stats.totalMembers > 5 &&
"Manque d'experts • "}
{cat.learners > stats.totalMembers * 0.3 &&
cat.mentors < 2 &&
"Besoin de mentors"}
</TooltipTrigger>
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
<div className="text-xs">
<p className="font-medium">
{skill.importance === "incontournable"
? "Compétence incontournable"
: "Compétence majeure"}
</p>
<p className="text-slate-400">
Objectif : {target}% de couverture
<br />
Actuel : {skill.coverage.toFixed(0)}%
<br />
{skill.coverage < target
? `Manque ${(
target - skill.coverage
).toFixed(0)}%`
: "Objectif atteint"}
</p>
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
)}
</div>
))}
{stats.learningNeeds > 0 && (

View File

@@ -13,7 +13,13 @@ import {
PieChart,
Pie,
Cell,
Legend,
} from "recharts";
import {
Tooltip as UITooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface TeamStatsProps {
stats: TeamReviewData["stats"];
@@ -73,20 +79,45 @@ export function TeamStats({
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>);
// 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;
const gapData = Object.entries(criticalGapsByCategory).map(
([category, count]) => ({
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,
value: count,
fill: "#ef4444",
})
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 (
@@ -117,12 +148,97 @@ export function TeamStats({
{mentorshipRatio.toFixed(0)}%
</p>
</div>
<UITooltip>
<TooltipTrigger asChild>
<div className="space-y-1">
<p className="text-sm text-slate-400">Gaps critiques</p>
<p className="text-sm text-slate-400">Compétences critiques</p>
<p className="text-2xl font-bold text-red-400">
{stats.learningNeeds}
{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 */}
@@ -198,9 +314,9 @@ export function TeamStats({
{gapData.length > 0 && (
<div>
<h3 className="text-sm font-medium text-slate-200 mb-4">
Gaps critiques par catégorie
Compétences critiques sous-couvertes par catégorie
</h3>
<div className="h-[200px]">
<div className="h-[250px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={gapData}>
<CartesianGrid
@@ -224,8 +340,36 @@ export function TeamStats({
}}
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"
/>
<Bar dataKey="value" />
</BarChart>
</ResponsiveContainer>
</div>

View File

@@ -12,6 +12,7 @@ export interface TeamMemberSkill {
skillId: string;
skillName: string;
category: string;
importance: "incontournable" | "majeure" | "standard";
level: "never" | "not-autonomous" | "autonomous" | "expert";
canMentor: boolean;
wantsToLearn: boolean;
@@ -32,6 +33,7 @@ export interface SkillGap {
skillName: string;
category: string;
icon?: string;
importance: "incontournable" | "majeure" | "standard";
teamMembers: number;
experts: number;
mentors: number;
@@ -48,6 +50,10 @@ export interface CategoryCoverage {
experts: number;
mentors: number;
learners: number;
criticalSkillsCoverage: {
incontournable: number;
majeure: number;
};
}
export interface TeamReviewData {
@@ -66,6 +72,10 @@ export interface TeamReviewData {
averageTeamLevel: number;
mentorshipOpportunities: number;
learningNeeds: number;
criticalSkillsCoverage: {
incontournable: number;
majeure: number;
};
};
}
@@ -74,6 +84,7 @@ export interface MentorOpportunity {
mentee: TeamMember;
skill: string;
category: string;
importance: "incontournable" | "majeure" | "standard";
mentorLevel: "autonomous" | "expert";
menteeLevel: "never" | "not-autonomous";
}

View File

@@ -32,6 +32,7 @@ export class TeamReviewService {
s.id as skill_id,
s.name as skill_name,
s.icon,
s.importance,
sc.name as category
FROM skill_categories sc
JOIN skills s ON s.category_id = sc.id
@@ -54,6 +55,7 @@ export class TeamReviewService {
u.email,
s.id as skill_id,
s.name as skill_name,
s.importance,
sc.name as category,
se.level,
se.can_mentor,
@@ -97,6 +99,7 @@ export class TeamReviewService {
skillId: row.skill_id,
skillName: row.skill_name,
category: row.category,
importance: row.importance || "standard",
level: row.level as SkillLevel,
canMentor: row.can_mentor || false,
wantsToLearn: row.wants_to_learn || false,
@@ -140,22 +143,28 @@ export class TeamReviewService {
const coverage =
totalTeamMembers > 0 ? (teamMembers / totalTeamMembers) * 100 : 0;
// Déterminer le niveau de risque en fonction de l'importance
const risk =
skill.importance === "incontournable" && experts === 0
? "high"
: skill.importance === "majeure" && experts === 0 && mentors === 0
? "high"
: experts === 0 && mentors === 0
? "medium"
: "low";
return {
skillId: skill.skill_id,
skillName: skill.skill_name,
category: skill.category,
icon: skill.icon,
importance: skill.importance || "standard",
team_members: teamMembers,
experts,
mentors,
learners,
coverage,
risk:
experts === 0 && mentors === 0
? "high"
: experts === 0 || mentors === 0
? "medium"
: "low",
risk,
};
});
@@ -172,6 +181,10 @@ export class TeamReviewService {
mentors: 0,
learners: 0,
coverage: 0,
criticalSkillsCoverage: {
incontournable: 0,
majeure: 0,
},
});
}
@@ -186,6 +199,17 @@ export class TeamReviewService {
categoryStats.experts += skillGap.experts;
categoryStats.mentors += skillGap.mentors;
categoryStats.learners += skillGap.learners;
// Calculer la couverture des compétences critiques
if (
skillGap.importance === "incontournable" &&
skillGap.coverage > 50
) {
categoryStats.criticalSkillsCoverage.incontournable++;
}
if (skillGap.importance === "majeure" && skillGap.coverage > 50) {
categoryStats.criticalSkillsCoverage.majeure++;
}
}
}
@@ -208,6 +232,28 @@ export class TeamReviewService {
);
// 8. Calculer les statistiques globales
const criticalSkillsCoverage = {
incontournable:
(skillGaps
.filter((gap) => gap.importance === "incontournable")
.reduce((acc, gap) => acc + (gap.coverage > 50 ? 1 : 0), 0) /
Math.max(
1,
skillGaps.filter((gap) => gap.importance === "incontournable")
.length
)) *
100,
majeure:
(skillGaps
.filter((gap) => gap.importance === "majeure")
.reduce((acc, gap) => acc + (gap.coverage > 50 ? 1 : 0), 0) /
Math.max(
1,
skillGaps.filter((gap) => gap.importance === "majeure").length
)) *
100,
};
const stats = {
totalMembers: membersMap.size,
totalSkills: skillGaps.length,
@@ -221,6 +267,7 @@ export class TeamReviewService {
0
),
learningNeeds: skillGaps.filter((gap) => gap.risk === "high").length,
criticalSkillsCoverage,
};
await client.query("COMMIT");
@@ -249,25 +296,43 @@ export class TeamReviewService {
): string[] {
const recommendations: string[] = [];
// Analyser les gaps critiques
const criticalGaps = skillGaps.filter((gap) => gap.risk === "high");
if (criticalGaps.length > 0) {
// Analyser les gaps critiques par importance
const uncoveredIncontournables = skillGaps.filter(
(gap) => gap.importance === "incontournable" && gap.coverage < 50
);
if (uncoveredIncontournables.length > 0) {
recommendations.push(
`Attention : ${
criticalGaps.length
} compétences critiques sans expert ni mentor : ${criticalGaps
`⚠️ ${
uncoveredIncontournables.length
} compétences incontournables faiblement couvertes : ${uncoveredIncontournables
.map((gap) => gap.skillName)
.join(", ")}`
);
}
// Analyser les opportunités de mentorat
const mentorshipNeeds = skillGaps.filter(
(gap) => gap.learners > 0 && gap.mentors > 0
).length;
if (mentorshipNeeds > 0) {
const uncoveredMajeures = skillGaps.filter(
(gap) => gap.importance === "majeure" && gap.coverage < 30
);
if (uncoveredMajeures.length > 0) {
recommendations.push(
`${mentorshipNeeds} opportunités de mentorat identifiées`
`⚠️ ${
uncoveredMajeures.length
} compétences majeures faiblement couvertes : ${uncoveredMajeures
.map((gap) => gap.skillName)
.join(", ")}`
);
}
// Analyser les opportunités de mentorat pour les compétences importantes
const criticalMentorshipNeeds = skillGaps.filter(
(gap) =>
gap.learners > 0 &&
gap.mentors > 0 &&
(gap.importance === "incontournable" || gap.importance === "majeure")
).length;
if (criticalMentorshipNeeds > 0) {
recommendations.push(
`${criticalMentorshipNeeds} opportunités de mentorat sur des compétences critiques identifiées`
);
}
@@ -283,13 +348,19 @@ export class TeamReviewService {
);
}
// Identifier les experts isolés
const isolatedExperts = members.filter(
(member) => member.expertSkills > 0 && member.mentorSkills === 0
// Identifier les experts isolés sur des compétences importantes
const isolatedExperts = members.filter((member) =>
member.skills.some(
(skill) =>
skill.level === "expert" &&
!skill.canMentor &&
(skill.importance === "incontournable" ||
skill.importance === "majeure")
)
);
if (isolatedExperts.length > 0) {
recommendations.push(
`${isolatedExperts.length} experts pourraient devenir mentors`
`${isolatedExperts.length} experts en compétences critiques pourraient devenir mentors`
);
}