refactor: rule of coverage are in one place
This commit is contained in:
@@ -6,6 +6,10 @@ import {
|
|||||||
getSkillLevelLabel,
|
getSkillLevelLabel,
|
||||||
getSkillLevelColor,
|
getSkillLevelColor,
|
||||||
} from "../team-detail/team-stats-row";
|
} from "../team-detail/team-stats-row";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface DirectionOverviewProps {
|
interface DirectionOverviewProps {
|
||||||
direction: string;
|
direction: string;
|
||||||
@@ -182,7 +186,10 @@ export function DirectionOverview({
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
averageCriticalCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
averageCriticalCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-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="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all shadow-sm ${
|
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-red-500 to-red-400"
|
||||||
: "bg-gradient-to-r from-green-500 to-green-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 text-slate-300">Majeures:</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
averageCriticalCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
averageCriticalCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-orange-400"
|
? "text-orange-400"
|
||||||
: "text-green-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="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all shadow-sm ${
|
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-orange-500 to-orange-400"
|
||||||
: "bg-gradient-to-r from-green-500 to-green-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 { TeamMetricsCards } from "./team-metrics-cards";
|
||||||
import { TeamDetailTabs } from "./team-detail-tabs";
|
import { TeamDetailTabs } from "./team-detail-tabs";
|
||||||
import { TeamMemberModal } from "@/components/admin";
|
import { TeamMemberModal } from "@/components/admin";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamDetailClientWrapperProps {
|
interface TeamDetailClientWrapperProps {
|
||||||
team: TeamStats;
|
team: TeamStats;
|
||||||
@@ -185,10 +189,14 @@ export function TeamDetailClientWrapper({
|
|||||||
),
|
),
|
||||||
skillGaps: {
|
skillGaps: {
|
||||||
incontournable: skillAnalysis.filter(
|
incontournable: skillAnalysis.filter(
|
||||||
(s) => s.importance === "incontournable" && s.coverage < 75
|
(s) =>
|
||||||
|
s.importance === "incontournable" &&
|
||||||
|
isCoverageBelowObjective(s.coverage, s.importance)
|
||||||
).length,
|
).length,
|
||||||
majeure: skillAnalysis.filter(
|
majeure: skillAnalysis.filter(
|
||||||
(s) => s.importance === "majeure" && s.coverage < 60
|
(s) =>
|
||||||
|
s.importance === "majeure" &&
|
||||||
|
isCoverageBelowObjective(s.coverage, s.importance)
|
||||||
).length,
|
).length,
|
||||||
standard: skillAnalysis.filter(
|
standard: skillAnalysis.filter(
|
||||||
(s) => s.importance === "standard" && s.averageLevel < 1.5
|
(s) => s.importance === "standard" && s.averageLevel < 1.5
|
||||||
@@ -196,10 +204,14 @@ export function TeamDetailClientWrapper({
|
|||||||
},
|
},
|
||||||
strongSkills: {
|
strongSkills: {
|
||||||
incontournable: skillAnalysis.filter(
|
incontournable: skillAnalysis.filter(
|
||||||
(s) => s.importance === "incontournable" && s.coverage >= 75
|
(s) =>
|
||||||
|
s.importance === "incontournable" &&
|
||||||
|
!isCoverageBelowObjective(s.coverage, s.importance)
|
||||||
).length,
|
).length,
|
||||||
majeure: skillAnalysis.filter(
|
majeure: skillAnalysis.filter(
|
||||||
(s) => s.importance === "majeure" && s.coverage >= 60
|
(s) =>
|
||||||
|
s.importance === "majeure" &&
|
||||||
|
!isCoverageBelowObjective(s.coverage, s.importance)
|
||||||
).length,
|
).length,
|
||||||
standard: skillAnalysis.filter(
|
standard: skillAnalysis.filter(
|
||||||
(s) => s.importance === "standard" && s.averageLevel >= 2.5
|
(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 { Users, ExternalLink, Download, Eye } from "lucide-react";
|
||||||
|
|
||||||
import { TeamMember } from "@/lib/admin-types";
|
import { TeamMember } from "@/lib/admin-types";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamDetailModalProps {
|
interface TeamDetailModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -157,7 +161,10 @@ export function TeamDetailModal({
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
team.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
team.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-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="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all ${
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
team.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
team.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "bg-red-500"
|
? "bg-red-500"
|
||||||
: "bg-green-500"
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
@@ -182,7 +192,10 @@ export function TeamDetailModal({
|
|||||||
<span className="text-sm text-slate-300">Majeures</span>
|
<span className="text-sm text-slate-300">Majeures</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
team.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
team.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-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="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all ${
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
team.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
team.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "bg-red-500"
|
? "bg-red-500"
|
||||||
: "bg-green-500"
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
@@ -209,13 +225,11 @@ export function TeamDetailModal({
|
|||||||
<h3 className="font-medium text-white mb-3">Top 3 Compétences</h3>
|
<h3 className="font-medium text-white mb-3">Top 3 Compétences</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{team.topSkills.slice(0, 3).map((skill, idx) => {
|
{team.topSkills.slice(0, 3).map((skill, idx) => {
|
||||||
const target =
|
const target = COVERAGE_OBJECTIVES[skill.importance];
|
||||||
skill.importance === "incontournable"
|
const isUnderTarget = isCoverageBelowObjective(
|
||||||
? 75
|
skill.coverage,
|
||||||
: skill.importance === "majeure"
|
skill.importance
|
||||||
? 60
|
);
|
||||||
: 0;
|
|
||||||
const isUnderTarget = target > 0 && skill.coverage < target;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { TrendingUp, MessageSquare, Lightbulb } from "lucide-react";
|
import { TrendingUp, MessageSquare, Lightbulb } from "lucide-react";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface SkillAnalysis {
|
interface SkillAnalysis {
|
||||||
skillName: string;
|
skillName: string;
|
||||||
@@ -64,9 +68,7 @@ export function TeamInsightsTab({
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{/* Incontournables */}
|
{/* Incontournables */}
|
||||||
{skillAnalysis
|
{skillAnalysis
|
||||||
.filter(
|
.filter((s) => isCoverageBelowObjective(s.coverage, s.importance))
|
||||||
(s) => s.importance === "incontournable" && s.coverage < 75
|
|
||||||
)
|
|
||||||
.map((skill, idx) => (
|
.map((skill, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -83,7 +85,7 @@ export function TeamInsightsTab({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-[10px] text-red-300 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="text-[10px] text-red-300 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
Objectif: 75%
|
Objectif: {COVERAGE_OBJECTIVES[skill.importance]}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-red-400 font-medium">
|
<div className="text-xs text-red-400 font-medium">
|
||||||
{skill.coverage.toFixed(0)}%
|
{skill.coverage.toFixed(0)}%
|
||||||
@@ -95,7 +97,7 @@ export function TeamInsightsTab({
|
|||||||
|
|
||||||
{/* Majeures */}
|
{/* Majeures */}
|
||||||
{skillAnalysis
|
{skillAnalysis
|
||||||
.filter((s) => s.importance === "majeure" && s.coverage < 60)
|
.filter((s) => isCoverageBelowObjective(s.coverage, s.importance))
|
||||||
.map((skill, idx) => (
|
.map((skill, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -112,7 +114,7 @@ export function TeamInsightsTab({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-[10px] text-blue-300 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="text-[10px] text-blue-300 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
Objectif: 60%
|
Objectif: {COVERAGE_OBJECTIVES[skill.importance]}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-blue-400 font-medium">
|
<div className="text-xs text-blue-400 font-medium">
|
||||||
{skill.coverage.toFixed(0)}%
|
{skill.coverage.toFixed(0)}%
|
||||||
@@ -217,7 +219,7 @@ export function TeamInsightsTab({
|
|||||||
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""}{" "}
|
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""}{" "}
|
||||||
incontournable
|
incontournable
|
||||||
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""} sous
|
{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" : ""} majeure
|
||||||
{teamInsights.skillGaps.majeure > 1 ? "s" : ""} n'atteigne
|
{teamInsights.skillGaps.majeure > 1 ? "s" : ""} n'atteigne
|
||||||
{teamInsights.skillGaps.majeure > 1 ? "nt" : ""} pas
|
{teamInsights.skillGaps.majeure > 1 ? "nt" : ""} pas
|
||||||
l'objectif de 60%.
|
l'objectif de {COVERAGE_OBJECTIVES.majeure}%.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Users, BarChart3, Award, BookOpen, Target } from "lucide-react";
|
import { Users, BarChart3, Award, BookOpen, Target } from "lucide-react";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamInsights {
|
interface TeamInsights {
|
||||||
averageTeamLevel: number;
|
averageTeamLevel: number;
|
||||||
@@ -81,7 +85,10 @@ export function TeamMetricsCards({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`text-2xl font-bold ${
|
className={`text-2xl font-bold ${
|
||||||
teamInsights.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -102,7 +109,10 @@ export function TeamMetricsCards({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`text-2xl font-bold ${
|
className={`text-2xl font-bold ${
|
||||||
teamInsights.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
import { BarChart3, Target, Star } from "lucide-react";
|
import { BarChart3, Target, Star } from "lucide-react";
|
||||||
import { TeamStats } from "@/lib/admin-types";
|
import { TeamStats } from "@/lib/admin-types";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface SkillAnalysis {
|
interface SkillAnalysis {
|
||||||
skillName: string;
|
skillName: string;
|
||||||
category: string;
|
category: string;
|
||||||
importance: "incontournable" | "majeure" | "standard";
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
|
icon?: string;
|
||||||
experts: Array<{
|
experts: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
level: number;
|
level: number;
|
||||||
@@ -125,15 +130,14 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className={`text-xs ${
|
className={`text-xs ${
|
||||||
skill.importance === "incontournable"
|
skill.importance === "standard"
|
||||||
? skill.coverage < 75
|
? "text-slate-400"
|
||||||
? "text-red-400"
|
: isCoverageBelowObjective(
|
||||||
: "text-green-400"
|
skill.coverage,
|
||||||
: skill.importance === "majeure"
|
skill.importance
|
||||||
? skill.coverage < 60
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
: "text-slate-400"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{skill.coverage.toFixed(0)}%
|
{skill.coverage.toFixed(0)}%
|
||||||
@@ -231,7 +235,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${
|
className={`text-sm font-medium ${
|
||||||
skill.coverage < 75
|
isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-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="w-16 bg-white/10 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full ${
|
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}%` }}
|
style={{ width: `${skill.coverage}%` }}
|
||||||
/>
|
/>
|
||||||
@@ -292,7 +304,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${
|
className={`text-sm font-medium ${
|
||||||
skill.coverage < 60
|
isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-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="w-16 bg-white/10 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full ${
|
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}%` }}
|
style={{ width: `${skill.coverage}%` }}
|
||||||
/>
|
/>
|
||||||
@@ -404,7 +424,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
teamInsights.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -417,7 +440,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="w-16 bg-white/10 rounded-full h-2">
|
<div className="w-16 bg-white/10 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${
|
className={`h-2 rounded-full ${
|
||||||
teamInsights.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "bg-red-500"
|
? "bg-red-500"
|
||||||
: "bg-green-500"
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
@@ -437,7 +463,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
teamInsights.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -447,7 +476,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="w-16 bg-white/10 rounded-full h-2">
|
<div className="w-16 bg-white/10 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${
|
className={`h-2 rounded-full ${
|
||||||
teamInsights.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "bg-red-500"
|
? "bg-red-500"
|
||||||
: "bg-green-500"
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface SkillAnalysis {
|
interface SkillAnalysis {
|
||||||
skillName: string;
|
skillName: string;
|
||||||
@@ -124,13 +128,11 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredSkills.map((skill, idx) => {
|
{filteredSkills.map((skill, idx) => {
|
||||||
const target =
|
const target = COVERAGE_OBJECTIVES[skill.importance];
|
||||||
skill.importance === "incontournable"
|
const isUnderTarget = isCoverageBelowObjective(
|
||||||
? 75
|
skill.coverage,
|
||||||
: skill.importance === "majeure"
|
skill.importance
|
||||||
? 60
|
);
|
||||||
: 0;
|
|
||||||
const isUnderTarget = target > 0 && skill.coverage < target;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamStatsCardProps {
|
interface TeamStatsCardProps {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -24,6 +28,8 @@ interface TeamStatsCardProps {
|
|||||||
averageLevel: number;
|
averageLevel: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
|
coverage: number;
|
||||||
}>;
|
}>;
|
||||||
skillCoverage: number;
|
skillCoverage: number;
|
||||||
onViewDetails?: () => void;
|
onViewDetails?: () => void;
|
||||||
@@ -52,10 +58,11 @@ export function getSkillLevelBadgeClasses(level: number): string {
|
|||||||
return "bg-green-500/20 border-green-500/30 text-green-300";
|
return "bg-green-500/20 border-green-500/30 text-green-300";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProgressColor(percentage: number): string {
|
export function getProgressColor(
|
||||||
if (percentage < 30) return "bg-red-500";
|
percentage: number,
|
||||||
if (percentage < 60) return "bg-orange-500";
|
importance: "incontournable" | "majeure" | "standard"
|
||||||
if (percentage < 80) return "bg-blue-500";
|
): string {
|
||||||
|
if (isCoverageBelowObjective(percentage, importance)) return "bg-red-500";
|
||||||
return "bg-green-500";
|
return "bg-green-500";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
import { getImportanceColors } from "@/lib/tech-colors";
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamStatsRowProps {
|
interface TeamStatsRowProps {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -70,10 +74,11 @@ export function getSkillLevelBadgeClasses(level: number): string {
|
|||||||
return "bg-green-500/20 border-green-500/30 text-green-300";
|
return "bg-green-500/20 border-green-500/30 text-green-300";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProgressColor(percentage: number): string {
|
export function getProgressColor(
|
||||||
if (percentage < 30) return "bg-red-500";
|
percentage: number,
|
||||||
if (percentage < 60) return "bg-orange-500";
|
importance: "incontournable" | "majeure" | "standard"
|
||||||
if (percentage < 80) return "bg-blue-500";
|
): string {
|
||||||
|
if (isCoverageBelowObjective(percentage, importance)) return "bg-red-500";
|
||||||
return "bg-green-500";
|
return "bg-green-500";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +95,14 @@ export function TeamStatsRow({
|
|||||||
onViewReport,
|
onViewReport,
|
||||||
}: TeamStatsRowProps) {
|
}: TeamStatsRowProps) {
|
||||||
// Calculer les alertes sur les compétences critiques
|
// Calculer les alertes sur les compétences critiques
|
||||||
const hasIncontournableAlert = criticalSkillsCoverage.incontournable < 75;
|
const hasIncontournableAlert = isCoverageBelowObjective(
|
||||||
const hasMajeureAlert = criticalSkillsCoverage.majeure < 60;
|
criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
);
|
||||||
|
const hasMajeureAlert = isCoverageBelowObjective(
|
||||||
|
criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
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">
|
<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-center">
|
||||||
<div
|
<div
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -158,7 +172,7 @@ export function TeamStatsRow({
|
|||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
Couverture des compétences incontournables
|
Couverture des compétences incontournables
|
||||||
<br />
|
<br />
|
||||||
Objectif : 75%
|
Objectif : {COVERAGE_OBJECTIVES.incontournable}%
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -170,7 +184,10 @@ export function TeamStatsRow({
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -184,7 +201,7 @@ export function TeamStatsRow({
|
|||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
Couverture des compétences majeures
|
Couverture des compétences majeures
|
||||||
<br />
|
<br />
|
||||||
Objectif : 60%
|
Objectif : {COVERAGE_OBJECTIVES.majeure}%
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -222,13 +239,11 @@ export function TeamStatsRow({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{topSkills.slice(0, 3).map((skill, idx) => {
|
{topSkills.slice(0, 3).map((skill, idx) => {
|
||||||
const colors = getImportanceColors(skill.importance);
|
const colors = getImportanceColors(skill.importance);
|
||||||
const target =
|
const target = COVERAGE_OBJECTIVES[skill.importance];
|
||||||
skill.importance === "incontournable"
|
const isUnderTarget = isCoverageBelowObjective(
|
||||||
? 75
|
skill.coverage,
|
||||||
: skill.importance === "majeure"
|
skill.importance
|
||||||
? 60
|
);
|
||||||
: 0;
|
|
||||||
const isUnderTarget = target > 0 && skill.coverage < target;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider key={idx}>
|
<TooltipProvider key={idx}>
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import { TeamMemberProfile, SkillGap } from "@/lib/team-review-types";
|
|||||||
import { UserCheck, GraduationCap } from "lucide-react";
|
import { UserCheck, GraduationCap } from "lucide-react";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
import { getImportanceColors } from "@/lib/tech-colors";
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
|
import {
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
SKILL_LEVEL_VALUES,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface SkillMatrixProps {
|
interface SkillMatrixProps {
|
||||||
members: TeamMemberProfile[];
|
members: TeamMemberProfile[];
|
||||||
@@ -26,6 +30,21 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
(skill) => skill.skillId && skill.skillName && skill.category
|
(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) => {
|
const skillsByCategory = validSkillGaps.reduce((acc, skill) => {
|
||||||
if (!acc[skill.category]) {
|
if (!acc[skill.category]) {
|
||||||
acc[skill.category] = [];
|
acc[skill.category] = [];
|
||||||
@@ -34,6 +53,11 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, SkillGap[]>);
|
}, {} 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 getLevelBadge = (level: string | null) => {
|
||||||
const colors = {
|
const colors = {
|
||||||
never: "bg-white/5 text-slate-300",
|
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="flex items-center gap-2">
|
||||||
<div className="w-full bg-white/10 rounded-full h-2">
|
<div className="w-full bg-white/10 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${colors.bg.replace(
|
className={`h-2 rounded-full ${
|
||||||
"/20",
|
isCoverageBelowObjective(
|
||||||
"/50"
|
skill.coverage || 0,
|
||||||
)}`}
|
skill.importance
|
||||||
|
)
|
||||||
|
? "bg-red-500/50"
|
||||||
|
: colors.bg.replace("/20", "/50")
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.max(
|
width: `${Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -189,7 +217,14 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<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)}%
|
{Math.round(skill.coverage || 0)}%
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
import { getImportanceColors } from "@/lib/tech-colors";
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -41,20 +45,20 @@ export function TeamOverview({
|
|||||||
const categoriesNeedingAttention = [...categoryCoverage]
|
const categoriesNeedingAttention = [...categoryCoverage]
|
||||||
.map((cat) => {
|
.map((cat) => {
|
||||||
// Pour chaque catégorie, on identifie :
|
// 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(
|
const uncoveredIncontournables = skillGaps.filter(
|
||||||
(gap) =>
|
(gap) =>
|
||||||
gap.category === cat.category &&
|
gap.category === cat.category &&
|
||||||
gap.importance === "incontournable" &&
|
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(
|
const uncoveredMajeures = skillGaps.filter(
|
||||||
(gap) =>
|
(gap) =>
|
||||||
gap.category === cat.category &&
|
gap.category === cat.category &&
|
||||||
gap.importance === "majeure" &&
|
gap.importance === "majeure" &&
|
||||||
gap.coverage < 60
|
isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Une catégorie nécessite de l'attention si :
|
// Une catégorie nécessite de l'attention si :
|
||||||
@@ -81,7 +85,7 @@ export function TeamOverview({
|
|||||||
attentionScore,
|
attentionScore,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter((cat): cat is NonNullable<typeof cat> => cat !== null)
|
||||||
.sort((a, b) => b.attentionScore - a.attentionScore)
|
.sort((a, b) => b.attentionScore - a.attentionScore)
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
|
||||||
@@ -190,8 +194,7 @@ export function TeamOverview({
|
|||||||
})
|
})
|
||||||
.map((skill) => {
|
.map((skill) => {
|
||||||
const colors = getImportanceColors(skill.importance);
|
const colors = getImportanceColors(skill.importance);
|
||||||
const target =
|
const target = COVERAGE_OBJECTIVES[skill.importance];
|
||||||
skill.importance === "incontournable" ? 75 : 60;
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={skill.skillId}>
|
<Tooltip key={skill.skillId}>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
@@ -203,7 +206,10 @@ export function TeamOverview({
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
skill.coverage < target
|
isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-slate-400"
|
: "text-slate-400"
|
||||||
}
|
}
|
||||||
@@ -224,7 +230,10 @@ export function TeamOverview({
|
|||||||
<br />
|
<br />
|
||||||
Actuel : {skill.coverage.toFixed(0)}%
|
Actuel : {skill.coverage.toFixed(0)}%
|
||||||
<br />
|
<br />
|
||||||
{skill.coverage < target
|
{isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? `Manque ${(
|
? `Manque ${(
|
||||||
target - skill.coverage
|
target - skill.coverage
|
||||||
).toFixed(0)}%`
|
).toFixed(0)}%`
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import {
|
|||||||
Cell,
|
Cell,
|
||||||
Legend,
|
Legend,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
import {
|
import {
|
||||||
Tooltip as UITooltip,
|
Tooltip as UITooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -82,9 +86,11 @@ export function TeamStats({
|
|||||||
// Gaps critiques par catégorie, séparés par importance
|
// Gaps critiques par catégorie, séparés par importance
|
||||||
const criticalGapsByCategory = skillGaps.reduce((acc, gap) => {
|
const criticalGapsByCategory = skillGaps.reduce((acc, gap) => {
|
||||||
const isIncontournableUndercovered =
|
const isIncontournableUndercovered =
|
||||||
gap.importance === "incontournable" && gap.coverage < 75;
|
gap.importance === "incontournable" &&
|
||||||
|
isCoverageBelowObjective(gap.coverage, gap.importance);
|
||||||
const isMajeureUndercovered =
|
const isMajeureUndercovered =
|
||||||
gap.importance === "majeure" && gap.coverage < 60;
|
gap.importance === "majeure" &&
|
||||||
|
isCoverageBelowObjective(gap.coverage, gap.importance);
|
||||||
|
|
||||||
if (isIncontournableUndercovered || isMajeureUndercovered) {
|
if (isIncontournableUndercovered || isMajeureUndercovered) {
|
||||||
if (!acc[gap.category]) {
|
if (!acc[gap.category]) {
|
||||||
@@ -161,8 +167,13 @@ export function TeamStats({
|
|||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1">
|
||||||
<p>Compétences critiques sous-couvertes :</p>
|
<p>Compétences critiques sous-couvertes :</p>
|
||||||
<ul className="list-disc list-inside space-y-0.5">
|
<ul className="list-disc list-inside space-y-0.5">
|
||||||
<li>Incontournables : couverture < 75%</li>
|
<li>
|
||||||
<li>Majeures : couverture < 60%</li>
|
Incontournables : couverture <{" "}
|
||||||
|
{COVERAGE_OBJECTIVES.incontournable}%
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Majeures : couverture < {COVERAGE_OBJECTIVES.majeure}%
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -205,7 +216,10 @@ export function TeamStats({
|
|||||||
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
|
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<p>Compétences incontournables</p>
|
<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>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</UITooltip>
|
</UITooltip>
|
||||||
@@ -343,16 +357,16 @@ export function TeamStats({
|
|||||||
formatter={(value, name) => {
|
formatter={(value, name) => {
|
||||||
const label =
|
const label =
|
||||||
name === "incontournable"
|
name === "incontournable"
|
||||||
? "Incontournables (obj. 75%)"
|
? `Incontournables (obj. ${COVERAGE_OBJECTIVES.incontournable}%)`
|
||||||
: "Majeures (obj. 60%)";
|
: `Majeures (obj. ${COVERAGE_OBJECTIVES.majeure}%)`;
|
||||||
return [value, label];
|
return [value, label];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Legend
|
<Legend
|
||||||
formatter={(value) => {
|
formatter={(value) => {
|
||||||
return value === "incontournable"
|
return value === "incontournable"
|
||||||
? "Incontournables (obj. 75%)"
|
? `Incontournables (obj. ${COVERAGE_OBJECTIVES.incontournable}%)`
|
||||||
: "Majeures (obj. 60%)";
|
: `Majeures (obj. ${COVERAGE_OBJECTIVES.majeure}%)`;
|
||||||
}}
|
}}
|
||||||
wrapperStyle={{
|
wrapperStyle={{
|
||||||
paddingTop: "20px",
|
paddingTop: "20px",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CategoryEvaluation,
|
CategoryEvaluation,
|
||||||
RadarChartData,
|
RadarChartData,
|
||||||
SkillCategory,
|
SkillCategory,
|
||||||
|
SkillImportance,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export function calculateCategoryScore(
|
export function calculateCategoryScore(
|
||||||
@@ -51,3 +52,47 @@ export function createEmptyEvaluation(
|
|||||||
selectedSkillIds: [],
|
selectedSkillIds: [],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const COVERAGE_OBJECTIVES: Record<SkillImportance, number> = {
|
||||||
|
incontournable: 75,
|
||||||
|
majeure: 60,
|
||||||
|
standard: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isCoverageBelowObjective(
|
||||||
|
coverage: number,
|
||||||
|
importance: SkillImportance
|
||||||
|
): boolean {
|
||||||
|
const objective = COVERAGE_OBJECTIVES[importance];
|
||||||
|
return objective > 0 && coverage < objective;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCoverageObjective(importance: SkillImportance): number {
|
||||||
|
return COVERAGE_OBJECTIVES[importance];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateSkillCoverage(
|
||||||
|
levels: number[],
|
||||||
|
totalMembers: number
|
||||||
|
): number {
|
||||||
|
if (levels.length === 0 || totalMembers === 0) return 0;
|
||||||
|
|
||||||
|
// Compter le nombre de membres autonomes ou experts (niveau >= 2)
|
||||||
|
const expertCount = levels.filter((level) => level >= 2).length;
|
||||||
|
|
||||||
|
// La couverture est le pourcentage de membres autonomes ou experts
|
||||||
|
return (expertCount / totalMembers) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSkillCoverageSQL(levelField: string): string {
|
||||||
|
return `
|
||||||
|
COALESCE(
|
||||||
|
(COUNT(*) FILTER (WHERE ${levelField} >= 2) * 100.0) / NULLIF(COUNT(*), 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExpertLevel(level: number): boolean {
|
||||||
|
return level >= 2;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ export const SKILL_LEVEL_VALUES: Record<Exclude<SkillLevel, null>, number> = {
|
|||||||
expert: 3,
|
expert: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SkillImportance = "incontournable" | "majeure" | "standard";
|
||||||
|
|
||||||
export interface Skill {
|
export interface Skill {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
links: string[];
|
links: string[];
|
||||||
importance?: string;
|
importance: SkillImportance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkillCategory {
|
export interface SkillCategory {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { getPool } from "./database";
|
|||||||
import { Team, SkillCategory } from "@/lib/types";
|
import { Team, SkillCategory } from "@/lib/types";
|
||||||
import { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
|
import { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
|
||||||
import { SkillsService } from "./skills-service";
|
import { SkillsService } from "./skills-service";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
generateSkillCoverageSQL,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
/**
|
/**
|
||||||
@@ -9,8 +13,11 @@ export class AdminService {
|
|||||||
*/
|
*/
|
||||||
static async getTeamsStats(): Promise<TeamStats[]> {
|
static async getTeamsStats(): Promise<TeamStats[]> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
// Récupérer toutes les équipes avec leurs membres et évaluations
|
// Récupérer toutes les équipes avec leurs membres et évaluations
|
||||||
const query = `
|
const query = `
|
||||||
WITH team_members AS (
|
WITH team_members AS (
|
||||||
@@ -56,14 +63,7 @@ export class AdminService {
|
|||||||
s.icon as skill_icon,
|
s.icon as skill_icon,
|
||||||
s.importance,
|
s.importance,
|
||||||
AVG(ss.level_numeric) as avg_level,
|
AVG(ss.level_numeric) as avg_level,
|
||||||
COALESCE(
|
${generateSkillCoverageSQL("ss.level_numeric")} as coverage
|
||||||
SUM(CASE
|
|
||||||
WHEN ss.level_numeric >= 2 THEN 100.0 -- autonomous ou expert
|
|
||||||
WHEN ss.level_numeric = 1 THEN 50.0 -- not-autonomous
|
|
||||||
ELSE 0.0 -- never
|
|
||||||
END) / NULLIF(COUNT(*), 0),
|
|
||||||
0
|
|
||||||
) as coverage
|
|
||||||
FROM skill_stats ss
|
FROM skill_stats ss
|
||||||
JOIN skills s ON ss.skill_id = s.id
|
JOIN skills s ON ss.skill_id = s.id
|
||||||
WHERE ss.skill_name IS NOT NULL
|
WHERE ss.skill_name IS NOT NULL
|
||||||
@@ -73,11 +73,17 @@ export class AdminService {
|
|||||||
SELECT
|
SELECT
|
||||||
team_id,
|
team_id,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
AVG(CASE WHEN importance = 'incontournable' THEN coverage ELSE NULL END),
|
AVG(CASE
|
||||||
|
WHEN importance = 'incontournable' THEN coverage
|
||||||
|
ELSE NULL
|
||||||
|
END),
|
||||||
0
|
0
|
||||||
) as incontournable_coverage,
|
) as incontournable_coverage,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
AVG(CASE WHEN importance = 'majeure' THEN coverage ELSE NULL END),
|
AVG(CASE
|
||||||
|
WHEN importance = 'majeure' THEN coverage
|
||||||
|
ELSE NULL
|
||||||
|
END),
|
||||||
0
|
0
|
||||||
) as majeure_coverage
|
) as majeure_coverage
|
||||||
FROM team_skill_averages
|
FROM team_skill_averages
|
||||||
@@ -149,7 +155,8 @@ export class AdminService {
|
|||||||
ORDER BY tm.direction, tm.team_name
|
ORDER BY tm.direction, tm.team_name
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await pool.query(query);
|
const result = await client.query(query);
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return result.rows.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
teamId: row.team_id,
|
teamId: row.team_id,
|
||||||
@@ -168,8 +175,11 @@ export class AdminService {
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching teams stats:", error);
|
console.error("Error fetching teams stats:", error);
|
||||||
throw new Error("Failed to fetch teams statistics");
|
throw new Error("Failed to fetch teams statistics");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,10 +251,15 @@ export class AdminService {
|
|||||||
skills: any[];
|
skills: any[];
|
||||||
}> {
|
}> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const [categoriesResult, skills] = await Promise.all([
|
const [categoriesResult, skills] = await Promise.all([
|
||||||
pool.query("SELECT id, name, icon FROM skill_categories ORDER BY name"),
|
client.query(
|
||||||
|
"SELECT id, name, icon FROM skill_categories ORDER BY name"
|
||||||
|
),
|
||||||
SkillsService.getAllSkillsWithUsage(),
|
SkillsService.getAllSkillsWithUsage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -254,13 +269,18 @@ export class AdminService {
|
|||||||
skills: [],
|
skills: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skillCategories,
|
skillCategories,
|
||||||
skills,
|
skills,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching skills page data:", error);
|
console.error("Error fetching skills page data:", error);
|
||||||
throw new Error("Failed to fetch skills page data");
|
throw new Error("Failed to fetch skills page data");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,13 +328,16 @@ export class AdminService {
|
|||||||
users: any[];
|
users: any[];
|
||||||
}> {
|
}> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const [teamsResult, usersResult] = await Promise.all([
|
const [teamsResult, usersResult] = await Promise.all([
|
||||||
pool.query(
|
client.query(
|
||||||
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
||||||
),
|
),
|
||||||
pool.query(`
|
client.query(`
|
||||||
SELECT
|
SELECT
|
||||||
u.uuid_id as uuid,
|
u.uuid_id as uuid,
|
||||||
u.first_name as "firstName",
|
u.first_name as "firstName",
|
||||||
@@ -330,13 +353,18 @@ export class AdminService {
|
|||||||
`),
|
`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams: teamsResult.rows,
|
teams: teamsResult.rows,
|
||||||
users: usersResult.rows,
|
users: usersResult.rows,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching users page data:", error);
|
console.error("Error fetching users page data:", error);
|
||||||
throw new Error("Failed to fetch users page data");
|
throw new Error("Failed to fetch users page data");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,10 +377,13 @@ export class AdminService {
|
|||||||
directionStats: DirectionStats[];
|
directionStats: DirectionStats[];
|
||||||
}> {
|
}> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const [teamsResult, teamStats] = await Promise.all([
|
const [teamsResult, teamStats] = await Promise.all([
|
||||||
pool.query(
|
client.query(
|
||||||
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
||||||
),
|
),
|
||||||
AdminService.getTeamsStats(),
|
AdminService.getTeamsStats(),
|
||||||
@@ -360,14 +391,19 @@ export class AdminService {
|
|||||||
|
|
||||||
const directionStats = AdminService.generateDirectionStats(teamStats);
|
const directionStats = AdminService.generateDirectionStats(teamStats);
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams: teamsResult.rows,
|
teams: teamsResult.rows,
|
||||||
teamStats,
|
teamStats,
|
||||||
directionStats,
|
directionStats,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching teams page data:", error);
|
console.error("Error fetching teams page data:", error);
|
||||||
throw new Error("Failed to fetch teams page data");
|
throw new Error("Failed to fetch teams page data");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,13 +417,18 @@ export class AdminService {
|
|||||||
directionStats: DirectionStats[];
|
directionStats: DirectionStats[];
|
||||||
}> {
|
}> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const [teamsResult, categoriesResult, teamStats] = await Promise.all([
|
const [teamsResult, categoriesResult, teamStats] = await Promise.all([
|
||||||
pool.query(
|
client.query(
|
||||||
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
||||||
),
|
),
|
||||||
pool.query("SELECT id, name, icon FROM skill_categories ORDER BY name"),
|
client.query(
|
||||||
|
"SELECT id, name, icon FROM skill_categories ORDER BY name"
|
||||||
|
),
|
||||||
AdminService.getTeamsStats(),
|
AdminService.getTeamsStats(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -400,6 +441,8 @@ export class AdminService {
|
|||||||
|
|
||||||
const directionStats = AdminService.generateDirectionStats(teamStats);
|
const directionStats = AdminService.generateDirectionStats(teamStats);
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams,
|
teams,
|
||||||
skillCategories,
|
skillCategories,
|
||||||
@@ -407,8 +450,11 @@ export class AdminService {
|
|||||||
directionStats,
|
directionStats,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching overview page data:", error);
|
console.error("Error fetching overview page data:", error);
|
||||||
throw new Error("Failed to fetch overview page data");
|
throw new Error("Failed to fetch overview page data");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import {
|
|||||||
TeamMember,
|
TeamMember,
|
||||||
TeamMemberSkill,
|
TeamMemberSkill,
|
||||||
} from "@/lib/team-review-types";
|
} from "@/lib/team-review-types";
|
||||||
import { SkillLevel } from "@/lib/types";
|
import { SkillLevel, SKILL_LEVEL_VALUES } from "@/lib/types";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
calculateSkillCoverage,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
export class TeamReviewService {
|
export class TeamReviewService {
|
||||||
static async getTeamReviewData(teamId: string): Promise<TeamReviewData> {
|
static async getTeamReviewData(teamId: string): Promise<TeamReviewData> {
|
||||||
@@ -100,7 +105,11 @@ export class TeamReviewService {
|
|||||||
skillName: row.skill_name,
|
skillName: row.skill_name,
|
||||||
category: row.category,
|
category: row.category,
|
||||||
importance: row.importance || "standard",
|
importance: row.importance || "standard",
|
||||||
level: row.level as SkillLevel,
|
level: row.level as
|
||||||
|
| "never"
|
||||||
|
| "not-autonomous"
|
||||||
|
| "autonomous"
|
||||||
|
| "expert",
|
||||||
canMentor: row.can_mentor || false,
|
canMentor: row.can_mentor || false,
|
||||||
wantsToLearn: row.wants_to_learn || false,
|
wantsToLearn: row.wants_to_learn || false,
|
||||||
};
|
};
|
||||||
@@ -140,8 +149,14 @@ export class TeamReviewService {
|
|||||||
const teamMembers = evaluations.filter((e) => e.level).length;
|
const teamMembers = evaluations.filter((e) => e.level).length;
|
||||||
|
|
||||||
const totalTeamMembers = membersMap.size;
|
const totalTeamMembers = membersMap.size;
|
||||||
const coverage =
|
|
||||||
totalTeamMembers > 0 ? (teamMembers / totalTeamMembers) * 100 : 0;
|
// Calculer la couverture en fonction des niveaux
|
||||||
|
const levels = evaluations.map((e) =>
|
||||||
|
e.level
|
||||||
|
? SKILL_LEVEL_VALUES[e.level as keyof typeof SKILL_LEVEL_VALUES]
|
||||||
|
: 0
|
||||||
|
);
|
||||||
|
const coverage = calculateSkillCoverage(levels, totalTeamMembers);
|
||||||
|
|
||||||
// Déterminer le niveau de risque en fonction de l'importance
|
// Déterminer le niveau de risque en fonction de l'importance
|
||||||
const risk =
|
const risk =
|
||||||
@@ -159,7 +174,7 @@ export class TeamReviewService {
|
|||||||
category: skill.category,
|
category: skill.category,
|
||||||
icon: skill.icon,
|
icon: skill.icon,
|
||||||
importance: skill.importance || "standard",
|
importance: skill.importance || "standard",
|
||||||
team_members: teamMembers,
|
teamMembers,
|
||||||
experts,
|
experts,
|
||||||
mentors,
|
mentors,
|
||||||
learners,
|
learners,
|
||||||
@@ -175,8 +190,8 @@ export class TeamReviewService {
|
|||||||
if (!categoriesMap.has(skill.category)) {
|
if (!categoriesMap.has(skill.category)) {
|
||||||
categoriesMap.set(skill.category, {
|
categoriesMap.set(skill.category, {
|
||||||
category: skill.category,
|
category: skill.category,
|
||||||
total_skills: 0,
|
totalSkills: 0,
|
||||||
covered_skills: 0,
|
coveredSkills: 0,
|
||||||
experts: 0,
|
experts: 0,
|
||||||
mentors: 0,
|
mentors: 0,
|
||||||
learners: 0,
|
learners: 0,
|
||||||
@@ -189,13 +204,13 @@ export class TeamReviewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categoryStats = categoriesMap.get(skill.category)!;
|
const categoryStats = categoriesMap.get(skill.category)!;
|
||||||
categoryStats.total_skills++;
|
categoryStats.totalSkills++;
|
||||||
|
|
||||||
const skillGap = skillGaps.find(
|
const skillGap = skillGaps.find(
|
||||||
(gap) => gap.skillId === skill.skill_id
|
(gap) => gap.skillId === skill.skill_id
|
||||||
);
|
);
|
||||||
if (skillGap) {
|
if (skillGap) {
|
||||||
if (skillGap.team_members > 0) categoryStats.covered_skills++;
|
if (skillGap.teamMembers > 0) categoryStats.coveredSkills++;
|
||||||
categoryStats.experts += skillGap.experts;
|
categoryStats.experts += skillGap.experts;
|
||||||
categoryStats.mentors += skillGap.mentors;
|
categoryStats.mentors += skillGap.mentors;
|
||||||
categoryStats.learners += skillGap.learners;
|
categoryStats.learners += skillGap.learners;
|
||||||
@@ -203,11 +218,14 @@ export class TeamReviewService {
|
|||||||
// Calculer la couverture des compétences critiques
|
// Calculer la couverture des compétences critiques
|
||||||
if (
|
if (
|
||||||
skillGap.importance === "incontournable" &&
|
skillGap.importance === "incontournable" &&
|
||||||
skillGap.coverage > 50
|
!isCoverageBelowObjective(skillGap.coverage, skillGap.importance)
|
||||||
) {
|
) {
|
||||||
categoryStats.criticalSkillsCoverage.incontournable++;
|
categoryStats.criticalSkillsCoverage.incontournable++;
|
||||||
}
|
}
|
||||||
if (skillGap.importance === "majeure" && skillGap.coverage > 50) {
|
if (
|
||||||
|
skillGap.importance === "majeure" &&
|
||||||
|
!isCoverageBelowObjective(skillGap.coverage, skillGap.importance)
|
||||||
|
) {
|
||||||
categoryStats.criticalSkillsCoverage.majeure++;
|
categoryStats.criticalSkillsCoverage.majeure++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,8 +237,8 @@ export class TeamReviewService {
|
|||||||
).map((category) => ({
|
).map((category) => ({
|
||||||
...category,
|
...category,
|
||||||
coverage:
|
coverage:
|
||||||
category.total_skills > 0
|
category.totalSkills > 0
|
||||||
? (category.covered_skills / category.total_skills) * 100
|
? (category.coveredSkills / category.totalSkills) * 100
|
||||||
: 0,
|
: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -236,7 +254,14 @@ export class TeamReviewService {
|
|||||||
incontournable:
|
incontournable:
|
||||||
(skillGaps
|
(skillGaps
|
||||||
.filter((gap) => gap.importance === "incontournable")
|
.filter((gap) => gap.importance === "incontournable")
|
||||||
.reduce((acc, gap) => acc + (gap.coverage > 50 ? 1 : 0), 0) /
|
.reduce(
|
||||||
|
(acc, gap) =>
|
||||||
|
acc +
|
||||||
|
(!isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
|
? 1
|
||||||
|
: 0),
|
||||||
|
0
|
||||||
|
) /
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
skillGaps.filter((gap) => gap.importance === "incontournable")
|
skillGaps.filter((gap) => gap.importance === "incontournable")
|
||||||
@@ -246,7 +271,14 @@ export class TeamReviewService {
|
|||||||
majeure:
|
majeure:
|
||||||
(skillGaps
|
(skillGaps
|
||||||
.filter((gap) => gap.importance === "majeure")
|
.filter((gap) => gap.importance === "majeure")
|
||||||
.reduce((acc, gap) => acc + (gap.coverage > 50 ? 1 : 0), 0) /
|
.reduce(
|
||||||
|
(acc, gap) =>
|
||||||
|
acc +
|
||||||
|
(!isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
|
? 1
|
||||||
|
: 0),
|
||||||
|
0
|
||||||
|
) /
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
skillGaps.filter((gap) => gap.importance === "majeure").length
|
skillGaps.filter((gap) => gap.importance === "majeure").length
|
||||||
@@ -298,7 +330,9 @@ export class TeamReviewService {
|
|||||||
|
|
||||||
// Analyser les gaps critiques par importance
|
// Analyser les gaps critiques par importance
|
||||||
const uncoveredIncontournables = skillGaps.filter(
|
const uncoveredIncontournables = skillGaps.filter(
|
||||||
(gap) => gap.importance === "incontournable" && gap.coverage < 50
|
(gap) =>
|
||||||
|
gap.importance === "incontournable" &&
|
||||||
|
isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
);
|
);
|
||||||
if (uncoveredIncontournables.length > 0) {
|
if (uncoveredIncontournables.length > 0) {
|
||||||
recommendations.push(
|
recommendations.push(
|
||||||
@@ -311,7 +345,9 @@ export class TeamReviewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uncoveredMajeures = skillGaps.filter(
|
const uncoveredMajeures = skillGaps.filter(
|
||||||
(gap) => gap.importance === "majeure" && gap.coverage < 30
|
(gap) =>
|
||||||
|
gap.importance === "majeure" &&
|
||||||
|
isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
);
|
);
|
||||||
if (uncoveredMajeures.length > 0) {
|
if (uncoveredMajeures.length > 0) {
|
||||||
recommendations.push(
|
recommendations.push(
|
||||||
@@ -338,7 +374,7 @@ export class TeamReviewService {
|
|||||||
|
|
||||||
// Analyser la couverture des catégories
|
// Analyser la couverture des catégories
|
||||||
const lowCoverageCategories = categoryCoverage
|
const lowCoverageCategories = categoryCoverage
|
||||||
.filter((cat) => cat.coverage < 50)
|
.filter((cat) => cat.coverage < COVERAGE_OBJECTIVES.majeure)
|
||||||
.map((cat) => cat.category);
|
.map((cat) => cat.category);
|
||||||
if (lowCoverageCategories.length > 0) {
|
if (lowCoverageCategories.length > 0) {
|
||||||
recommendations.push(
|
recommendations.push(
|
||||||
|
|||||||
Reference in New Issue
Block a user