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

@@ -6,6 +6,10 @@ import {
getSkillLevelLabel,
getSkillLevelColor,
} from "../team-detail/team-stats-row";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface DirectionOverviewProps {
direction: string;
@@ -182,7 +186,10 @@ export function DirectionOverview({
</span>
<span
className={`text-sm font-bold ${
averageCriticalCoverage.incontournable < 75
isCoverageBelowObjective(
averageCriticalCoverage.incontournable,
"incontournable"
)
? "text-red-400"
: "text-green-400"
}`}
@@ -193,7 +200,10 @@ export function DirectionOverview({
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all shadow-sm ${
averageCriticalCoverage.incontournable < 75
isCoverageBelowObjective(
averageCriticalCoverage.incontournable,
"incontournable"
)
? "bg-gradient-to-r from-red-500 to-red-400"
: "bg-gradient-to-r from-green-500 to-green-400"
}`}
@@ -210,7 +220,10 @@ export function DirectionOverview({
<span className="text-sm text-slate-300">Majeures:</span>
<span
className={`text-sm font-bold ${
averageCriticalCoverage.majeure < 60
isCoverageBelowObjective(
averageCriticalCoverage.majeure,
"majeure"
)
? "text-orange-400"
: "text-green-400"
}`}
@@ -221,7 +234,10 @@ export function DirectionOverview({
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all shadow-sm ${
averageCriticalCoverage.majeure < 60
isCoverageBelowObjective(
averageCriticalCoverage.majeure,
"majeure"
)
? "bg-gradient-to-r from-orange-500 to-orange-400"
: "bg-gradient-to-r from-green-500 to-green-400"
}`}

View File

@@ -6,6 +6,10 @@ import { TeamDetailHeader } from "./team-detail-header";
import { TeamMetricsCards } from "./team-metrics-cards";
import { TeamDetailTabs } from "./team-detail-tabs";
import { TeamMemberModal } from "@/components/admin";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface TeamDetailClientWrapperProps {
team: TeamStats;
@@ -185,10 +189,14 @@ export function TeamDetailClientWrapper({
),
skillGaps: {
incontournable: skillAnalysis.filter(
(s) => s.importance === "incontournable" && s.coverage < 75
(s) =>
s.importance === "incontournable" &&
isCoverageBelowObjective(s.coverage, s.importance)
).length,
majeure: skillAnalysis.filter(
(s) => s.importance === "majeure" && s.coverage < 60
(s) =>
s.importance === "majeure" &&
isCoverageBelowObjective(s.coverage, s.importance)
).length,
standard: skillAnalysis.filter(
(s) => s.importance === "standard" && s.averageLevel < 1.5
@@ -196,10 +204,14 @@ export function TeamDetailClientWrapper({
},
strongSkills: {
incontournable: skillAnalysis.filter(
(s) => s.importance === "incontournable" && s.coverage >= 75
(s) =>
s.importance === "incontournable" &&
!isCoverageBelowObjective(s.coverage, s.importance)
).length,
majeure: skillAnalysis.filter(
(s) => s.importance === "majeure" && s.coverage >= 60
(s) =>
s.importance === "majeure" &&
!isCoverageBelowObjective(s.coverage, s.importance)
).length,
standard: skillAnalysis.filter(
(s) => s.importance === "standard" && s.averageLevel >= 2.5

View File

@@ -14,6 +14,10 @@ import { Badge } from "@/components/ui/badge";
import { Users, ExternalLink, Download, Eye } from "lucide-react";
import { TeamMember } from "@/lib/admin-types";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface TeamDetailModalProps {
isOpen: boolean;
@@ -157,7 +161,10 @@ export function TeamDetailModal({
</span>
<span
className={`text-sm font-bold ${
team.criticalSkillsCoverage.incontournable < 75
isCoverageBelowObjective(
team.criticalSkillsCoverage.incontournable,
"incontournable"
)
? "text-red-400"
: "text-green-400"
}`}
@@ -168,7 +175,10 @@ export function TeamDetailModal({
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all ${
team.criticalSkillsCoverage.incontournable < 75
isCoverageBelowObjective(
team.criticalSkillsCoverage.incontournable,
"incontournable"
)
? "bg-red-500"
: "bg-green-500"
}`}
@@ -182,7 +192,10 @@ export function TeamDetailModal({
<span className="text-sm text-slate-300">Majeures</span>
<span
className={`text-sm font-bold ${
team.criticalSkillsCoverage.majeure < 60
isCoverageBelowObjective(
team.criticalSkillsCoverage.majeure,
"majeure"
)
? "text-red-400"
: "text-green-400"
}`}
@@ -193,7 +206,10 @@ export function TeamDetailModal({
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all ${
team.criticalSkillsCoverage.majeure < 60
isCoverageBelowObjective(
team.criticalSkillsCoverage.majeure,
"majeure"
)
? "bg-red-500"
: "bg-green-500"
}`}
@@ -209,13 +225,11 @@ export function TeamDetailModal({
<h3 className="font-medium text-white mb-3">Top 3 Compétences</h3>
<div className="space-y-2">
{team.topSkills.slice(0, 3).map((skill, idx) => {
const target =
skill.importance === "incontournable"
? 75
: skill.importance === "majeure"
? 60
: 0;
const isUnderTarget = target > 0 && skill.coverage < target;
const target = COVERAGE_OBJECTIVES[skill.importance];
const isUnderTarget = isCoverageBelowObjective(
skill.coverage,
skill.importance
);
return (
<div

View File

@@ -1,6 +1,10 @@
"use client";
import { TrendingUp, MessageSquare, Lightbulb } from "lucide-react";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface SkillAnalysis {
skillName: string;
@@ -64,9 +68,7 @@ export function TeamInsightsTab({
<div className="space-y-1">
{/* Incontournables */}
{skillAnalysis
.filter(
(s) => s.importance === "incontournable" && s.coverage < 75
)
.filter((s) => isCoverageBelowObjective(s.coverage, s.importance))
.map((skill, idx) => (
<div
key={idx}
@@ -83,7 +85,7 @@ export function TeamInsightsTab({
</div>
<div className="flex items-center gap-2">
<div className="text-[10px] text-red-300 opacity-0 group-hover:opacity-100 transition-opacity">
Objectif: 75%
Objectif: {COVERAGE_OBJECTIVES[skill.importance]}%
</div>
<div className="text-xs text-red-400 font-medium">
{skill.coverage.toFixed(0)}%
@@ -95,7 +97,7 @@ export function TeamInsightsTab({
{/* Majeures */}
{skillAnalysis
.filter((s) => s.importance === "majeure" && s.coverage < 60)
.filter((s) => isCoverageBelowObjective(s.coverage, s.importance))
.map((skill, idx) => (
<div
key={idx}
@@ -112,7 +114,7 @@ export function TeamInsightsTab({
</div>
<div className="flex items-center gap-2">
<div className="text-[10px] text-blue-300 opacity-0 group-hover:opacity-100 transition-opacity">
Objectif: 60%
Objectif: {COVERAGE_OBJECTIVES[skill.importance]}%
</div>
<div className="text-xs text-blue-400 font-medium">
{skill.coverage.toFixed(0)}%
@@ -217,7 +219,7 @@ export function TeamInsightsTab({
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""}{" "}
incontournable
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""} sous
l'objectif de 75%.
l'objectif de {COVERAGE_OBJECTIVES.incontournable}%.
</>
) : (
<>
@@ -239,7 +241,7 @@ export function TeamInsightsTab({
{teamInsights.skillGaps.majeure > 1 ? "s" : ""} majeure
{teamInsights.skillGaps.majeure > 1 ? "s" : ""} n'atteigne
{teamInsights.skillGaps.majeure > 1 ? "nt" : ""} pas
l'objectif de 60%.
l'objectif de {COVERAGE_OBJECTIVES.majeure}%.
</>
) : (
<>

View File

@@ -1,6 +1,10 @@
"use client";
import { Users, BarChart3, Award, BookOpen, Target } from "lucide-react";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface TeamInsights {
averageTeamLevel: number;
@@ -81,7 +85,10 @@ export function TeamMetricsCards({
</div>
<div
className={`text-2xl font-bold ${
teamInsights.criticalSkillsCoverage.incontournable < 75
isCoverageBelowObjective(
teamInsights.criticalSkillsCoverage.incontournable,
"incontournable"
)
? "text-red-400"
: "text-green-400"
}`}
@@ -102,7 +109,10 @@ export function TeamMetricsCards({
</div>
<div
className={`text-2xl font-bold ${
teamInsights.criticalSkillsCoverage.majeure < 60
isCoverageBelowObjective(
teamInsights.criticalSkillsCoverage.majeure,
"majeure"
)
? "text-red-400"
: "text-green-400"
}`}

View File

@@ -3,11 +3,16 @@
import { BarChart3, Target, Star } from "lucide-react";
import { TeamStats } from "@/lib/admin-types";
import { TechIcon } from "@/components/icons/tech-icon";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface SkillAnalysis {
skillName: string;
category: string;
importance: "incontournable" | "majeure" | "standard";
icon?: string;
experts: Array<{
name: string;
level: number;
@@ -125,15 +130,14 @@ export function TeamOverviewTab({
<div className="flex items-center gap-2">
<div
className={`text-xs ${
skill.importance === "incontournable"
? skill.coverage < 75
? "text-red-400"
: "text-green-400"
: skill.importance === "majeure"
? skill.coverage < 60
? "text-red-400"
: "text-green-400"
: "text-slate-400"
skill.importance === "standard"
? "text-slate-400"
: isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? "text-red-400"
: "text-green-400"
}`}
>
{skill.coverage.toFixed(0)}%
@@ -231,7 +235,10 @@ export function TeamOverviewTab({
<div className="flex items-center gap-3">
<span
className={`text-sm font-medium ${
skill.coverage < 75
isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? "text-red-400"
: "text-green-400"
}`}
@@ -241,7 +248,12 @@ export function TeamOverviewTab({
<div className="w-16 bg-white/10 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full ${
skill.coverage < 75 ? "bg-red-500" : "bg-green-500"
isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? "bg-red-500"
: "bg-green-500"
}`}
style={{ width: `${skill.coverage}%` }}
/>
@@ -292,7 +304,10 @@ export function TeamOverviewTab({
<div className="flex items-center gap-3">
<span
className={`text-sm font-medium ${
skill.coverage < 60
isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? "text-red-400"
: "text-green-400"
}`}
@@ -302,7 +317,12 @@ export function TeamOverviewTab({
<div className="w-16 bg-white/10 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full ${
skill.coverage < 60 ? "bg-red-500" : "bg-green-500"
isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? "bg-red-500"
: "bg-green-500"
}`}
style={{ width: `${skill.coverage}%` }}
/>
@@ -404,7 +424,10 @@ export function TeamOverviewTab({
<div className="flex items-center gap-3">
<span
className={`text-sm font-bold ${
teamInsights.criticalSkillsCoverage.incontournable < 75
isCoverageBelowObjective(
teamInsights.criticalSkillsCoverage.incontournable,
"incontournable"
)
? "text-red-400"
: "text-green-400"
}`}
@@ -417,7 +440,10 @@ export function TeamOverviewTab({
<div className="w-16 bg-white/10 rounded-full h-2">
<div
className={`h-2 rounded-full ${
teamInsights.criticalSkillsCoverage.incontournable < 75
isCoverageBelowObjective(
teamInsights.criticalSkillsCoverage.incontournable,
"incontournable"
)
? "bg-red-500"
: "bg-green-500"
}`}
@@ -437,7 +463,10 @@ export function TeamOverviewTab({
<div className="flex items-center gap-3">
<span
className={`text-sm font-bold ${
teamInsights.criticalSkillsCoverage.majeure < 60
isCoverageBelowObjective(
teamInsights.criticalSkillsCoverage.majeure,
"majeure"
)
? "text-red-400"
: "text-green-400"
}`}
@@ -447,7 +476,10 @@ export function TeamOverviewTab({
<div className="w-16 bg-white/10 rounded-full h-2">
<div
className={`h-2 rounded-full ${
teamInsights.criticalSkillsCoverage.majeure < 60
isCoverageBelowObjective(
teamInsights.criticalSkillsCoverage.majeure,
"majeure"
)
? "bg-red-500"
: "bg-green-500"
}`}

View File

@@ -3,6 +3,10 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface SkillAnalysis {
skillName: string;
@@ -124,13 +128,11 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
</thead>
<tbody>
{filteredSkills.map((skill, idx) => {
const target =
skill.importance === "incontournable"
? 75
: skill.importance === "majeure"
? 60
: 0;
const isUnderTarget = target > 0 && skill.coverage < target;
const target = COVERAGE_OBJECTIVES[skill.importance];
const isUnderTarget = isCoverageBelowObjective(
skill.coverage,
skill.importance
);
return (
<tr

View File

@@ -12,6 +12,10 @@ import {
ChevronRight,
} from "lucide-react";
import { TechIcon } from "@/components/icons/tech-icon";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface TeamStatsCardProps {
teamId: string;
@@ -24,6 +28,8 @@ interface TeamStatsCardProps {
averageLevel: number;
color?: string;
icon?: string;
importance: "incontournable" | "majeure" | "standard";
coverage: number;
}>;
skillCoverage: number;
onViewDetails?: () => void;
@@ -52,10 +58,11 @@ export function getSkillLevelBadgeClasses(level: number): string {
return "bg-green-500/20 border-green-500/30 text-green-300";
}
export function getProgressColor(percentage: number): string {
if (percentage < 30) return "bg-red-500";
if (percentage < 60) return "bg-orange-500";
if (percentage < 80) return "bg-blue-500";
export function getProgressColor(
percentage: number,
importance: "incontournable" | "majeure" | "standard"
): string {
if (isCoverageBelowObjective(percentage, importance)) return "bg-red-500";
return "bg-green-500";
}

View File

@@ -24,6 +24,10 @@ import {
} from "lucide-react";
import { TechIcon } from "@/components/icons/tech-icon";
import { getImportanceColors } from "@/lib/tech-colors";
import {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
interface TeamStatsRowProps {
teamId: string;
@@ -70,10 +74,11 @@ export function getSkillLevelBadgeClasses(level: number): string {
return "bg-green-500/20 border-green-500/30 text-green-300";
}
export function getProgressColor(percentage: number): string {
if (percentage < 30) return "bg-red-500";
if (percentage < 60) return "bg-orange-500";
if (percentage < 80) return "bg-blue-500";
export function getProgressColor(
percentage: number,
importance: "incontournable" | "majeure" | "standard"
): string {
if (isCoverageBelowObjective(percentage, importance)) return "bg-red-500";
return "bg-green-500";
}
@@ -90,8 +95,14 @@ export function TeamStatsRow({
onViewReport,
}: TeamStatsRowProps) {
// Calculer les alertes sur les compétences critiques
const hasIncontournableAlert = criticalSkillsCoverage.incontournable < 75;
const hasMajeureAlert = criticalSkillsCoverage.majeure < 60;
const hasIncontournableAlert = isCoverageBelowObjective(
criticalSkillsCoverage.incontournable,
"incontournable"
);
const hasMajeureAlert = isCoverageBelowObjective(
criticalSkillsCoverage.majeure,
"majeure"
);
return (
<div className="group bg-gradient-to-r from-slate-800/50 to-slate-700/40 backdrop-blur-sm border border-slate-600/40 rounded-xl p-4 hover:from-slate-700/60 hover:to-slate-600/50 hover:border-slate-500/50 transition-all duration-300 shadow-lg hover:shadow-xl">
@@ -144,7 +155,10 @@ export function TeamStatsRow({
<div className="text-center">
<div
className={`text-sm font-bold ${
criticalSkillsCoverage.incontournable < 75
isCoverageBelowObjective(
criticalSkillsCoverage.incontournable,
"incontournable"
)
? "text-red-400"
: "text-green-400"
}`}
@@ -158,7 +172,7 @@ export function TeamStatsRow({
<p className="text-xs">
Couverture des compétences incontournables
<br />
Objectif : 75%
Objectif : {COVERAGE_OBJECTIVES.incontournable}%
</p>
</TooltipContent>
</Tooltip>
@@ -170,7 +184,10 @@ export function TeamStatsRow({
<div className="text-center">
<div
className={`text-sm font-bold ${
criticalSkillsCoverage.majeure < 60
isCoverageBelowObjective(
criticalSkillsCoverage.majeure,
"majeure"
)
? "text-red-400"
: "text-green-400"
}`}
@@ -184,7 +201,7 @@ export function TeamStatsRow({
<p className="text-xs">
Couverture des compétences majeures
<br />
Objectif : 60%
Objectif : {COVERAGE_OBJECTIVES.majeure}%
</p>
</TooltipContent>
</Tooltip>
@@ -222,13 +239,11 @@ export function TeamStatsRow({
<div className="flex items-center gap-2">
{topSkills.slice(0, 3).map((skill, idx) => {
const colors = getImportanceColors(skill.importance);
const target =
skill.importance === "incontournable"
? 75
: skill.importance === "majeure"
? 60
: 0;
const isUnderTarget = target > 0 && skill.coverage < target;
const target = COVERAGE_OBJECTIVES[skill.importance];
const isUnderTarget = isCoverageBelowObjective(
skill.coverage,
skill.importance
);
return (
<TooltipProvider key={idx}>