Files
peakskills/components/team-review/skill-matrix.tsx
2025-08-27 14:57:10 +02:00

395 lines
17 KiB
TypeScript

"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
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";
import { isCoverageBelowObjective } from "@/lib/evaluation-utils";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface SkillMatrixProps {
members: TeamMemberProfile[];
skillGaps: SkillGap[];
}
export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
// Filtrer les skills valides et les organiser par catégorie
const validSkillGaps = skillGaps.filter(
(skill) => skill.skillId && skill.skillName && skill.category
);
// Fonction de tri par importance
const sortByImportance = (a: SkillGap, b: SkillGap) => {
const importanceOrder = {
incontournable: 2,
majeure: 1,
standard: 0,
};
const importanceDiff =
importanceOrder[b.importance] - importanceOrder[a.importance];
if (importanceDiff !== 0) return importanceDiff;
// Si même importance, trier par couverture (ascendant)
return (a.coverage || 0) - (b.coverage || 0);
};
const skillsByCategory = validSkillGaps.reduce((acc, skill) => {
if (!acc[skill.category]) {
acc[skill.category] = [];
}
acc[skill.category].push(skill);
return acc;
}, {} as Record<string, SkillGap[]>);
// Trier les compétences par importance dans chaque catégorie
Object.values(skillsByCategory).forEach((skills) => {
skills.sort(sortByImportance);
});
const getLevelBadge = (level: string | null) => {
const colors = {
never: "bg-white/5 text-slate-300",
"not-autonomous": "bg-yellow-500/20 text-yellow-200",
autonomous: "bg-green-500/20 text-green-200",
expert: "bg-blue-500/20 text-blue-200",
};
return level ? (
<Badge
variant="secondary"
className={colors[level as keyof typeof colors]}
>
{level}
</Badge>
) : (
<span className="text-slate-600">-</span>
);
};
// Vérifier si nous avons des données valides
if (validSkillGaps.length === 0 || members.length === 0) {
return (
<Card className="bg-white/5 border-white/10 backdrop-blur">
<CardHeader>
<CardTitle className="text-slate-200">
Matrice des compétences
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-400">
Aucune donnée disponible pour la matrice des compétences.
</p>
</CardContent>
</Card>
);
}
return (
<Card className="bg-white/5 border-white/10 backdrop-blur">
<CardHeader>
<CardTitle className="text-slate-200">
Matrice des compétences
</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="all" className="w-full">
<TabsList className="mb-4 bg-white/5 border-white/10">
<TabsTrigger
value="all"
className="data-[state=active]:bg-white/10"
>
Toutes
</TabsTrigger>
{Object.keys(skillsByCategory).map((category) => (
<TabsTrigger
key={category}
value={category}
className="data-[state=active]:bg-white/10"
>
{category}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="all">
<div className="rounded-md border border-white/10">
<Table>
<TableHeader>
<TableRow className="border-white/10">
<TableHead className="w-[200px] text-slate-300">
Compétence
</TableHead>
<TableHead className="w-[120px] text-slate-300">
Catégorie
</TableHead>
{members.map((member) => (
<TableHead
key={`header-${member.member.uuid}`}
className="text-slate-300"
>
{member.member.firstName} {member.member.lastName}
</TableHead>
))}
<TableHead className="w-[120px] text-slate-300">
Couverture
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{validSkillGaps.sort(sortByImportance).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 ${colors.bg} ${colors.border} border flex items-center justify-center`}
>
<TechIcon
iconName={skill.icon || ""}
className={`w-4 h-4 ${colors.text}`}
fallbackText={skill.skillName}
/>
</div>
<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 w-fit"
>
Risque
</Badge>
)}
</div>
</div>
</TableCell>
<TableCell>
<Badge
variant="outline"
className="bg-white/5 text-slate-300 border-white/10"
>
{skill.category}
</Badge>
</TableCell>
{members.map((member) => {
const memberSkill = member.skills.find(
(s) => s.skillId === skill.skillId
);
return (
<TableCell
key={`skill-${skill.skillId}-member-${member.member.uuid}`}
>
<div className="flex items-center gap-2 flex-wrap">
{getLevelBadge(memberSkill?.level || null)}
{memberSkill?.canMentor && (
<Badge
variant="outline"
className="flex items-center gap-1 bg-green-500/10 text-green-200 border-green-500/30"
>
<UserCheck className="h-3 w-3" />
<span className="text-xs">Mentor</span>
</Badge>
)}
{memberSkill?.wantsToLearn && (
<Badge
variant="outline"
className="flex items-center gap-1 bg-blue-500/10 text-blue-200 border-blue-500/30"
>
<GraduationCap className="h-3 w-3" />
<span className="text-xs">Apprenant</span>
</Badge>
)}
</div>
</TableCell>
);
})}
<TableCell>
<div className="flex items-center gap-2">
<div className="w-full bg-white/10 rounded-full h-2">
<div
className={`h-2 rounded-full ${
(skill.coverage || 0) >= 75
? "bg-green-500/50"
: (skill.coverage || 0) >= 50
? "bg-yellow-500/50"
: "bg-red-500/50"
}`}
style={{
width: `${Math.max(
0,
Math.min(100, skill.coverage || 0)
)}%`,
}}
/>
</div>
<span
className={`text-sm whitespace-nowrap ${
(skill.coverage || 0) >= 75
? "text-green-400"
: (skill.coverage || 0) >= 50
? "text-yellow-400"
: "text-red-400"
}`}
>
{Math.round(skill.coverage || 0)}%
</span>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</TabsContent>
{Object.entries(skillsByCategory).map(([category, skills]) => (
<TabsContent key={category} value={category}>
<div className="rounded-md border border-white/10">
<Table>
<TableHeader>
<TableRow className="border-white/10">
<TableHead className="w-[200px] text-slate-300">
Compétence
</TableHead>
{members.map((member) => (
<TableHead
key={`header-${member.member.uuid}`}
className="text-slate-300"
>
{member.member.firstName} {member.member.lastName}
</TableHead>
))}
<TableHead className="w-[120px] text-slate-300">
Couverture
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{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 ${colors.bg} ${colors.border} border flex items-center justify-center`}
>
<TechIcon
iconName={skill.icon || ""}
className={`w-4 h-4 ${colors.text}`}
fallbackText={skill.skillName}
/>
</div>
<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 w-fit"
>
Risque
</Badge>
)}
</div>
</div>
</TableCell>
{members.map((member) => {
const memberSkill = member.skills.find(
(s) => s.skillId === skill.skillId
);
return (
<TableCell
key={`skill-${skill.skillId}-member-${member.member.uuid}`}
>
<div className="flex items-center gap-2 flex-wrap">
{getLevelBadge(memberSkill?.level || null)}
{memberSkill?.canMentor && (
<Badge
variant="outline"
className="flex items-center gap-1 bg-green-500/10 text-green-200 border-green-500/30"
>
<UserCheck className="h-3 w-3" />
<span className="text-xs">Mentor</span>
</Badge>
)}
{memberSkill?.wantsToLearn && (
<Badge
variant="outline"
className="flex items-center gap-1 bg-blue-500/10 text-blue-200 border-blue-500/30"
>
<GraduationCap className="h-3 w-3" />
<span className="text-xs">Apprenant</span>
</Badge>
)}
</div>
</TableCell>
);
})}
<TableCell>
<div className="flex items-center gap-2">
<div className="w-full bg-white/10 rounded-full h-2">
<div
className={`h-2 rounded-full ${
(skill.coverage || 0) >= 75
? "bg-green-500/50"
: (skill.coverage || 0) >= 50
? "bg-yellow-500/50"
: "bg-red-500/50"
}`}
style={{
width: `${Math.max(
0,
Math.min(100, skill.coverage || 0)
)}%`,
}}
/>
</div>
<span
className={`text-sm whitespace-nowrap ${
(skill.coverage || 0) >= 75
? "text-green-400"
: (skill.coverage || 0) >= 50
? "text-yellow-400"
: "text-red-400"
}`}
>
{Math.round(skill.coverage || 0)}%
</span>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</TabsContent>
))}
</Tabs>
</CardContent>
</Card>
);
}