Top 3 Compétences
- {team.topSkills.slice(0, 3).map((skill, idx) => (
-
-
{skill.skillName}
-
-
- {((skill.averageLevel / 3) * 100).toFixed(0)}%
-
-
+ {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;
+
+ return (
+
+
+
+ {skill.skillName}
+
+
+ {skill.importance === "incontournable"
+ ? "Incontournable"
+ : skill.importance === "majeure"
+ ? "Majeure"
+ : "Standard"}
+
+
+
+
+
+ {skill.coverage.toFixed(0)}%
+
+ {target > 0 && (
+
+ (obj. {target}%)
+
+ )}
+
+
+
-
- ))}
+ );
+ })}
diff --git a/components/admin/team-detail/team-stats-row.tsx b/components/admin/team-detail/team-stats-row.tsx
index 48392fb..f6475af 100644
--- a/components/admin/team-detail/team-stats-row.tsx
+++ b/components/admin/team-detail/team-stats-row.tsx
@@ -20,8 +20,10 @@ import {
User,
Zap,
Crown,
+ AlertTriangle,
} from "lucide-react";
import { TechIcon } from "@/components/icons/tech-icon";
+import { getImportanceColors } from "@/lib/tech-colors";
interface TeamStatsRowProps {
teamId: string;
@@ -34,8 +36,14 @@ interface TeamStatsRowProps {
averageLevel: number;
color?: string;
icon?: string;
+ importance: "incontournable" | "majeure" | "standard";
+ coverage: number;
}>;
skillCoverage: number;
+ criticalSkillsCoverage: {
+ incontournable: number;
+ majeure: number;
+ };
onViewDetails?: () => void;
onViewReport?: () => void;
}
@@ -77,9 +85,14 @@ export function TeamStatsRow({
averageSkillLevel,
topSkills,
skillCoverage,
+ criticalSkillsCoverage,
onViewDetails,
onViewReport,
}: TeamStatsRowProps) {
+ // Calculer les alertes sur les compétences critiques
+ const hasIncontournableAlert = criticalSkillsCoverage.incontournable < 75;
+ const hasMajeureAlert = criticalSkillsCoverage.majeure < 60;
+
return (
{/* Layout horizontal compact */}
@@ -125,19 +138,57 @@ export function TeamStatsRow({
{/* Indicateurs clés compacts */}
-
-
- {averageSkillLevel.toFixed(1)}
-
-
/ 3.0
-
+
+
+
+
+
+ {criticalSkillsCoverage.incontournable.toFixed(0)}%
+
+
Incont.
+
+
+
+
+ Couverture des compétences incontournables
+
+ Objectif : 75%
+
+
+
+
-
-
- {skillCoverage.toFixed(0)}%
-
-
Couv.
-
+
+
+
+
+
+ {criticalSkillsCoverage.majeure.toFixed(0)}%
+
+
Maj.
+
+
+
+
+ Couverture des compétences majeures
+
+ Objectif : 60%
+
+
+
+
@@ -169,21 +220,68 @@ export function TeamStatsRow({
{/* Top skills mini */}
- {topSkills.slice(0, 3).map((skill, idx) => (
-
-
- {skill.skillName}
-
-
- {((skill.averageLevel / 3) * 100).toFixed(0)}%
-
-
- ))}
+ {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;
+
+ return (
+
+
+
+
+
+ {skill.skillName}
+
+
+ {skill.coverage.toFixed(0)}%
+
+
+
+
+
+
{skill.skillName}
+
+ {skill.importance === "incontournable"
+ ? "Compétence incontournable"
+ : skill.importance === "majeure"
+ ? "Compétence majeure"
+ : "Compétence standard"}
+
+ {target > 0 && (
+
+ Objectif : {target}%
+
+ )}
+
+
+
+
+ );
+ })}
{/* Actions compactes */}
diff --git a/lib/admin-types.ts b/lib/admin-types.ts
index 3b09f05..41a7b29 100644
--- a/lib/admin-types.ts
+++ b/lib/admin-types.ts
@@ -21,8 +21,18 @@ export interface TeamStats {
direction: string;
totalMembers: number;
averageSkillLevel: number;
- topSkills: Array<{ skillName: string; averageLevel: number; icon?: string }>;
+ topSkills: Array<{
+ skillName: string;
+ averageLevel: number;
+ icon?: string;
+ importance: "incontournable" | "majeure" | "standard";
+ coverage: number;
+ }>;
skillCoverage: number; // Percentage of skills evaluated
+ criticalSkillsCoverage: {
+ incontournable: number;
+ majeure: number;
+ };
members: TeamMember[];
}
diff --git a/services/admin-service.ts b/services/admin-service.ts
index b8c1e03..ab079eb 100644
--- a/services/admin-service.ts
+++ b/services/admin-service.ts
@@ -31,6 +31,7 @@ export class AdminService {
tm.uuid_id,
s.id as skill_id,
s.name as skill_name,
+ s.importance,
sc.name as category_name,
CASE
WHEN se.level = 'never' THEN 0
@@ -53,11 +54,34 @@ export class AdminService {
ss.team_id,
ss.skill_name,
s.icon as skill_icon,
- AVG(ss.level_numeric) as avg_level
+ s.importance,
+ AVG(ss.level_numeric) as avg_level,
+ COALESCE(
+ 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
JOIN skills s ON ss.skill_id = s.id
WHERE ss.skill_name IS NOT NULL
- GROUP BY ss.team_id, ss.skill_name, s.icon
+ GROUP BY ss.team_id, ss.skill_name, s.icon, s.importance
+ ),
+ critical_skills_coverage AS (
+ SELECT
+ team_id,
+ COALESCE(
+ AVG(CASE WHEN importance = 'incontournable' THEN coverage ELSE NULL END),
+ 0
+ ) as incontournable_coverage,
+ COALESCE(
+ AVG(CASE WHEN importance = 'majeure' THEN coverage ELSE NULL END),
+ 0
+ ) as majeure_coverage
+ FROM team_skill_averages
+ GROUP BY team_id
)
SELECT
tm.team_id,
@@ -92,13 +116,26 @@ export class AdminService {
jsonb_build_object(
'skillName', tsa.skill_name,
'averageLevel', tsa.avg_level,
- 'icon', tsa.skill_icon
- ) ORDER BY tsa.avg_level DESC
+ 'icon', tsa.skill_icon,
+ 'importance', tsa.importance,
+ 'coverage', tsa.coverage
+ ) ORDER BY
+ CASE tsa.importance
+ WHEN 'incontournable' THEN 2
+ WHEN 'majeure' THEN 1
+ ELSE 0
+ END DESC,
+ tsa.avg_level DESC,
+ tsa.coverage DESC
)
FROM team_skill_averages tsa
WHERE tsa.team_id = tm.team_id
LIMIT 3
) as top_skills,
+ jsonb_build_object(
+ 'incontournable', COALESCE(csc.incontournable_coverage, 0),
+ 'majeure', COALESCE(csc.majeure_coverage, 0)
+ ) as critical_skills_coverage,
CASE
WHEN COUNT(DISTINCT ss.skill_id) > 0 THEN
(COUNT(DISTINCT ss.skill_id) * 100.0 / (SELECT COUNT(*) FROM skills))
@@ -106,7 +143,8 @@ export class AdminService {
END as skill_coverage
FROM team_members tm
LEFT JOIN skill_stats ss ON tm.team_id = ss.team_id AND tm.uuid_id = ss.uuid_id
- GROUP BY tm.team_id, tm.team_name, tm.direction
+ LEFT JOIN critical_skills_coverage csc ON tm.team_id = csc.team_id
+ GROUP BY tm.team_id, tm.team_name, tm.direction, csc.incontournable_coverage, csc.majeure_coverage
ORDER BY tm.direction, tm.team_name
`;
@@ -120,6 +158,10 @@ export class AdminService {
averageSkillLevel: parseFloat(row.avg_skill_level) || 0,
topSkills: row.top_skills || [],
skillCoverage: parseFloat(row.skill_coverage) || 0,
+ criticalSkillsCoverage: row.critical_skills_coverage || {
+ incontournable: 0,
+ majeure: 0,
+ },
members: (row.members || []).filter(
(member: any) => member.uuid !== null
),
@@ -238,14 +280,15 @@ export class AdminService {
);
if (parseInt(skillsCheck.rows[0].count) > 0) {
- throw new Error("Impossible de supprimer une catégorie qui contient des skills");
+ throw new Error(
+ "Impossible de supprimer une catégorie qui contient des skills"
+ );
}
// Supprimer la catégorie
- await client.query(
- "DELETE FROM skill_categories WHERE id = $1",
- [categoryId]
- );
+ await client.query("DELETE FROM skill_categories WHERE id = $1", [
+ categoryId,
+ ]);
await client.query("COMMIT");
} catch (error) {