refactor: rule of coverage are in one place
This commit is contained in:
@@ -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"
|
||||
}`}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}%.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}%`
|
||||
|
||||
@@ -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 < 75%</li>
|
||||
<li>Majeures : couverture < 60%</li>
|
||||
<li>
|
||||
Incontournables : couverture <{" "}
|
||||
{COVERAGE_OBJECTIVES.incontournable}%
|
||||
</li>
|
||||
<li>
|
||||
Majeures : couverture < {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",
|
||||
|
||||
Reference in New Issue
Block a user