From e9aecca2a5bd649e48c4e57981394e666d16cac3 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 27 Aug 2025 13:16:39 +0200 Subject: [PATCH] feat: review admin overview and popup details fwith importance --- .../admin/overview/direction-overview.tsx | 92 ++++++++- .../admin/team-detail/team-detail-modal.tsx | 178 ++++++++++++++---- .../admin/team-detail/team-stats-row.tsx | 152 ++++++++++++--- lib/admin-types.ts | 12 +- services/admin-service.ts | 63 ++++++- 5 files changed, 415 insertions(+), 82 deletions(-) diff --git a/components/admin/overview/direction-overview.tsx b/components/admin/overview/direction-overview.tsx index 1baa7ec..28c6490 100644 --- a/components/admin/overview/direction-overview.tsx +++ b/components/admin/overview/direction-overview.tsx @@ -15,8 +15,17 @@ interface DirectionOverviewProps { direction: string; totalMembers: number; averageSkillLevel: number; - topSkills: Array<{ skillName: string; averageLevel: number }>; + topSkills: Array<{ + skillName: string; + averageLevel: number; + importance: "incontournable" | "majeure" | "standard"; + coverage: number; + }>; skillCoverage: number; + criticalSkillsCoverage: { + incontournable: number; + majeure: number; + }; }>; totalMembers: number; averageSkillLevel: number; @@ -81,6 +90,18 @@ export function DirectionOverview({ }: DirectionOverviewProps) { const colors = getDirectionColors(direction); + // Calculer la moyenne des couvertures des compétences critiques + const averageCriticalCoverage = teams.reduce( + (acc, team) => { + acc.incontournable += team.criticalSkillsCoverage.incontournable; + acc.majeure += team.criticalSkillsCoverage.majeure; + return acc; + }, + { incontournable: 0, majeure: 0 } + ); + averageCriticalCoverage.incontournable /= teams.length || 1; + averageCriticalCoverage.majeure /= teams.length || 1; + return (
+ {/* Compétences incontournables */}
- Maîtrise globale: + Incontournables: - - {((averageSkillLevel / 3) * 100).toFixed(0)}% + + {averageCriticalCoverage.incontournable.toFixed(0)}%
-
+
-
- Basé sur {teams.length} équipes + + {/* Compétences majeures */} +
+
+ Majeures: + + {averageCriticalCoverage.majeure.toFixed(0)}% + +
+
+
+
+
+ + {/* Niveau global */} +
+
+ Niveau global: + + {((averageSkillLevel / 3) * 100).toFixed(0)}% + +
+
+
+
@@ -229,6 +302,7 @@ export function DirectionOverview({ averageSkillLevel={team.averageSkillLevel} topSkills={team.topSkills} skillCoverage={team.skillCoverage} + criticalSkillsCoverage={team.criticalSkillsCoverage} onViewDetails={() => onViewTeamDetails(team)} onViewReport={() => onExportTeamReport(team)} /> diff --git a/components/admin/team-detail/team-detail-modal.tsx b/components/admin/team-detail/team-detail-modal.tsx index d49dca1..9b41591 100644 --- a/components/admin/team-detail/team-detail-modal.tsx +++ b/components/admin/team-detail/team-detail-modal.tsx @@ -28,8 +28,14 @@ interface TeamDetailModalProps { skillName: string; averageLevel: number; icon?: string; + importance: "incontournable" | "majeure" | "standard"; + coverage: number; }>; skillCoverage: number; + criticalSkillsCoverage: { + incontournable: number; + majeure: number; + }; members: TeamMember[]; } | null; } @@ -113,27 +119,88 @@ export function TeamDetailModal({
{/* Stats générales */} -
-
- -
- {team.totalMembers} +
+
+

+ Équipe +

+
+
+ +
+
+ {team.totalMembers} +
+
Membres
+
+
+
+ +
+
+ {((team.averageSkillLevel / 3) * 100).toFixed(0)}% +
+
Niveau moyen
+
+
-
Membres
-
- -
- {((team.averageSkillLevel / 3) * 100).toFixed(0)}% + +
+

+ Couverture +

+
+
+ + Incontournables + + + {team.criticalSkillsCoverage.incontournable.toFixed(0)}% + +
+
+
+
+ +
+ Majeures + + {team.criticalSkillsCoverage.majeure.toFixed(0)}% + +
+
+
+
-
Niveau moyen
-
-
- -
- {team.skillCoverage.toFixed(0)}% -
-
Couverture
@@ -141,24 +208,65 @@ export function TeamDetailModal({

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) {