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

@@ -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>
</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"}
</div>
{/* 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>
</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 && (