refactor: rule of coverage are in one place

This commit is contained in:
Julien Froidefond
2025-08-27 14:31:05 +02:00
parent a5bcdd34fb
commit a8cad0b2ec
16 changed files with 430 additions and 133 deletions

View File

@@ -14,6 +14,10 @@ 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,
SKILL_LEVEL_VALUES,
} from "@/lib/evaluation-utils";
interface SkillMatrixProps {
members: TeamMemberProfile[];
@@ -26,6 +30,21 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
(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 (décroissant)
return (b.coverage || 0) - (a.coverage || 0);
};
const skillsByCategory = validSkillGaps.reduce((acc, skill) => {
if (!acc[skill.category]) {
acc[skill.category] = [];
@@ -34,6 +53,11 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
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",
@@ -176,10 +200,14 @@ 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={`h-2 rounded-full ${colors.bg.replace(
"/20",
"/50"
)}`}
className={`h-2 rounded-full ${
isCoverageBelowObjective(
skill.coverage || 0,
skill.importance
)
? "bg-red-500/50"
: colors.bg.replace("/20", "/50")
}`}
style={{
width: `${Math.max(
0,
@@ -189,7 +217,14 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
/>
</div>
<span
className={`text-sm ${colors.text} whitespace-nowrap`}
className={`text-sm whitespace-nowrap ${
isCoverageBelowObjective(
skill.coverage || 0,
skill.importance
)
? "text-red-400"
: colors.text
}`}
>
{Math.round(skill.coverage || 0)}%
</span>

View File

@@ -6,6 +6,10 @@ import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
import { AlertTriangle } from "lucide-react";
import { getImportanceColors } from "@/lib/tech-colors";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
import {
Tooltip,
TooltipContent,
@@ -41,20 +45,20 @@ export function TeamOverview({
const categoriesNeedingAttention = [...categoryCoverage]
.map((cat) => {
// Pour chaque catégorie, on identifie :
// 1. Les compétences incontournables sous-couvertes (< 75%)
// 1. Les compétences incontournables sous-couvertes
const uncoveredIncontournables = skillGaps.filter(
(gap) =>
gap.category === cat.category &&
gap.importance === "incontournable" &&
gap.coverage < 75
isCoverageBelowObjective(gap.coverage, gap.importance)
);
// 2. Les compétences majeures sous-couvertes (< 60%)
// 2. Les compétences majeures sous-couvertes
const uncoveredMajeures = skillGaps.filter(
(gap) =>
gap.category === cat.category &&
gap.importance === "majeure" &&
gap.coverage < 60
isCoverageBelowObjective(gap.coverage, gap.importance)
);
// Une catégorie nécessite de l'attention si :
@@ -81,7 +85,7 @@ export function TeamOverview({
attentionScore,
};
})
.filter(Boolean)
.filter((cat): cat is NonNullable<typeof cat> => cat !== null)
.sort((a, b) => b.attentionScore - a.attentionScore)
.slice(0, 3);
@@ -190,8 +194,7 @@ export function TeamOverview({
})
.map((skill) => {
const colors = getImportanceColors(skill.importance);
const target =
skill.importance === "incontournable" ? 75 : 60;
const target = COVERAGE_OBJECTIVES[skill.importance];
return (
<Tooltip key={skill.skillId}>
<TooltipTrigger>
@@ -203,7 +206,10 @@ export function TeamOverview({
</span>
<span
className={
skill.coverage < target
isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? "text-red-400"
: "text-slate-400"
}
@@ -224,7 +230,10 @@ export function TeamOverview({
<br />
Actuel : {skill.coverage.toFixed(0)}%
<br />
{skill.coverage < target
{isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? `Manque ${(
target - skill.coverage
).toFixed(0)}%`

View File

@@ -15,6 +15,10 @@ import {
Cell,
Legend,
} from "recharts";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
import {
Tooltip as UITooltip,
TooltipContent,
@@ -82,9 +86,11 @@ export function TeamStats({
// Gaps critiques par catégorie, séparés par importance
const criticalGapsByCategory = skillGaps.reduce((acc, gap) => {
const isIncontournableUndercovered =
gap.importance === "incontournable" && gap.coverage < 75;
gap.importance === "incontournable" &&
isCoverageBelowObjective(gap.coverage, gap.importance);
const isMajeureUndercovered =
gap.importance === "majeure" && gap.coverage < 60;
gap.importance === "majeure" &&
isCoverageBelowObjective(gap.coverage, gap.importance);
if (isIncontournableUndercovered || isMajeureUndercovered) {
if (!acc[gap.category]) {
@@ -161,8 +167,13 @@ export function TeamStats({
<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>
<li>
Incontournables : couverture &lt;{" "}
{COVERAGE_OBJECTIVES.incontournable}%
</li>
<li>
Majeures : couverture &lt; {COVERAGE_OBJECTIVES.majeure}%
</li>
</ul>
</div>
</TooltipContent>
@@ -205,7 +216,10 @@ export function TeamStats({
<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>
<p className="text-slate-400">
Objectif : {COVERAGE_OBJECTIVES.incontournable}% de
couverture
</p>
</div>
</TooltipContent>
</UITooltip>
@@ -343,16 +357,16 @@ export function TeamStats({
formatter={(value, name) => {
const label =
name === "incontournable"
? "Incontournables (obj. 75%)"
: "Majeures (obj. 60%)";
? `Incontournables (obj. ${COVERAGE_OBJECTIVES.incontournable}%)`
: `Majeures (obj. ${COVERAGE_OBJECTIVES.majeure}%)`;
return [value, label];
}}
/>
<Legend
formatter={(value) => {
return value === "incontournable"
? "Incontournables (obj. 75%)"
: "Majeures (obj. 60%)";
? `Incontournables (obj. ${COVERAGE_OBJECTIVES.incontournable}%)`
: `Majeures (obj. ${COVERAGE_OBJECTIVES.majeure}%)`;
}}
wrapperStyle={{
paddingTop: "20px",