From a8cad0b2ec24a2f0302244b4e7baab28c9244e5f Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 27 Aug 2025 14:31:05 +0200 Subject: [PATCH] refactor: rule of coverage are in one place --- .../admin/overview/direction-overview.tsx | 24 +++++- .../team-detail-client-wrapper.tsx | 20 ++++- .../admin/team-detail/team-detail-modal.tsx | 36 ++++++--- .../admin/team-detail/team-insights-tab.tsx | 18 +++-- .../admin/team-detail/team-metrics-cards.tsx | 14 +++- .../admin/team-detail/team-overview-tab.tsx | 66 +++++++++++---- .../admin/team-detail/team-skills-tab.tsx | 16 ++-- .../admin/team-detail/team-stats-card.tsx | 15 +++- .../admin/team-detail/team-stats-row.tsx | 49 ++++++++---- components/team-review/skill-matrix.tsx | 45 +++++++++-- components/team-review/team-overview.tsx | 27 ++++--- components/team-review/team-stats.tsx | 32 +++++--- lib/evaluation-utils.ts | 45 +++++++++++ lib/types.ts | 4 +- services/admin-service.ts | 80 +++++++++++++++---- services/team-review-service.ts | 72 ++++++++++++----- 16 files changed, 430 insertions(+), 133 deletions(-) diff --git a/components/admin/overview/direction-overview.tsx b/components/admin/overview/direction-overview.tsx index 28c6490..59e4aaa 100644 --- a/components/admin/overview/direction-overview.tsx +++ b/components/admin/overview/direction-overview.tsx @@ -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({
Majeures:
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 diff --git a/components/admin/team-detail/team-detail-modal.tsx b/components/admin/team-detail/team-detail-modal.tsx index 9b41591..0a62d5e 100644 --- a/components/admin/team-detail/team-detail-modal.tsx +++ b/components/admin/team-detail/team-detail-modal.tsx @@ -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({
Majeures
Top 3 Compétences
{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 (
{/* Incontournables */} {skillAnalysis - .filter( - (s) => s.importance === "incontournable" && s.coverage < 75 - ) + .filter((s) => isCoverageBelowObjective(s.coverage, s.importance)) .map((skill, idx) => (
- Objectif: 75% + Objectif: {COVERAGE_OBJECTIVES[skill.importance]}%
{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) => (
- Objectif: 60% + Objectif: {COVERAGE_OBJECTIVES[skill.importance]}%
{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}%. ) : ( <> diff --git a/components/admin/team-detail/team-metrics-cards.tsx b/components/admin/team-detail/team-metrics-cards.tsx index 2c5edc0..db4d437 100644 --- a/components/admin/team-detail/team-metrics-cards.tsx +++ b/components/admin/team-detail/team-metrics-cards.tsx @@ -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({
{skill.coverage.toFixed(0)}% @@ -231,7 +235,10 @@ export function TeamOverviewTab({
@@ -292,7 +304,10 @@ export function TeamOverviewTab({
@@ -404,7 +424,10 @@ export function TeamOverviewTab({
{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 ( ; 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"; } diff --git a/components/admin/team-detail/team-stats-row.tsx b/components/admin/team-detail/team-stats-row.tsx index f6475af..9e3c09d 100644 --- a/components/admin/team-detail/team-stats-row.tsx +++ b/components/admin/team-detail/team-stats-row.tsx @@ -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 (
@@ -144,7 +155,10 @@ export function TeamStatsRow({
Couverture des compétences incontournables
- Objectif : 75% + Objectif : {COVERAGE_OBJECTIVES.incontournable}%

@@ -170,7 +184,10 @@ export function TeamStatsRow({
Couverture des compétences majeures
- Objectif : 60% + Objectif : {COVERAGE_OBJECTIVES.majeure}%

@@ -222,13 +239,11 @@ export function TeamStatsRow({
{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 ( diff --git a/components/team-review/skill-matrix.tsx b/components/team-review/skill-matrix.tsx index a771067..d9e2ee4 100644 --- a/components/team-review/skill-matrix.tsx +++ b/components/team-review/skill-matrix.tsx @@ -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); + // 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) {
{Math.round(skill.coverage || 0)}% diff --git a/components/team-review/team-overview.tsx b/components/team-review/team-overview.tsx index 69ef0d0..c57bfc1 100644 --- a/components/team-review/team-overview.tsx +++ b/components/team-review/team-overview.tsx @@ -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 => 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 ( @@ -203,7 +206,10 @@ export function TeamOverview({ Actuel : {skill.coverage.toFixed(0)}%
- {skill.coverage < target + {isCoverageBelowObjective( + skill.coverage, + skill.importance + ) ? `Manque ${( target - skill.coverage ).toFixed(0)}%` diff --git a/components/team-review/team-stats.tsx b/components/team-review/team-stats.tsx index cda2686..a76678b 100644 --- a/components/team-review/team-stats.tsx +++ b/components/team-review/team-stats.tsx @@ -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({

Compétences critiques sous-couvertes :

    -
  • Incontournables : couverture < 75%
  • -
  • Majeures : couverture < 60%
  • +
  • + Incontournables : couverture <{" "} + {COVERAGE_OBJECTIVES.incontournable}% +
  • +
  • + Majeures : couverture < {COVERAGE_OBJECTIVES.majeure}% +
@@ -205,7 +216,10 @@ export function TeamStats({

Compétences incontournables

-

Objectif : 75% de couverture

+

+ Objectif : {COVERAGE_OBJECTIVES.incontournable}% de + couverture +

@@ -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]; }} /> { return value === "incontournable" - ? "Incontournables (obj. 75%)" - : "Majeures (obj. 60%)"; + ? `Incontournables (obj. ${COVERAGE_OBJECTIVES.incontournable}%)` + : `Majeures (obj. ${COVERAGE_OBJECTIVES.majeure}%)`; }} wrapperStyle={{ paddingTop: "20px", diff --git a/lib/evaluation-utils.ts b/lib/evaluation-utils.ts index 549ed76..6703bbc 100644 --- a/lib/evaluation-utils.ts +++ b/lib/evaluation-utils.ts @@ -3,6 +3,7 @@ import { CategoryEvaluation, RadarChartData, SkillCategory, + SkillImportance, } from "./types"; export function calculateCategoryScore( @@ -51,3 +52,47 @@ export function createEmptyEvaluation( selectedSkillIds: [], })); } + +export const COVERAGE_OBJECTIVES: Record = { + 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; +} diff --git a/lib/types.ts b/lib/types.ts index 9b6267b..3e5709f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -19,13 +19,15 @@ export const SKILL_LEVEL_VALUES: Record, number> = { expert: 3, }; +export type SkillImportance = "incontournable" | "majeure" | "standard"; + export interface Skill { id: string; name: string; description: string; icon?: string; links: string[]; - importance?: string; + importance: SkillImportance; } export interface SkillCategory { diff --git a/services/admin-service.ts b/services/admin-service.ts index 1b84e37..66f8dda 100644 --- a/services/admin-service.ts +++ b/services/admin-service.ts @@ -2,6 +2,10 @@ import { getPool } from "./database"; import { Team, SkillCategory } from "@/lib/types"; import { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types"; import { SkillsService } from "./skills-service"; +import { + COVERAGE_OBJECTIVES, + generateSkillCoverageSQL, +} from "@/lib/evaluation-utils"; export class AdminService { /** @@ -9,8 +13,11 @@ export class AdminService { */ static async getTeamsStats(): Promise { const pool = getPool(); + const client = await pool.connect(); try { + await client.query("BEGIN"); + // Récupérer toutes les équipes avec leurs membres et évaluations const query = ` WITH team_members AS ( @@ -56,14 +63,7 @@ export class AdminService { s.icon as skill_icon, 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 + ${generateSkillCoverageSQL("ss.level_numeric")} as coverage FROM skill_stats ss JOIN skills s ON ss.skill_id = s.id WHERE ss.skill_name IS NOT NULL @@ -73,11 +73,17 @@ export class AdminService { SELECT team_id, COALESCE( - AVG(CASE WHEN importance = 'incontournable' THEN coverage ELSE NULL END), + 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), + AVG(CASE + WHEN importance = 'majeure' THEN coverage + ELSE NULL + END), 0 ) as majeure_coverage FROM team_skill_averages @@ -149,7 +155,8 @@ export class AdminService { 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) => ({ teamId: row.team_id, @@ -168,8 +175,11 @@ export class AdminService { ), })); } catch (error) { + await client.query("ROLLBACK"); console.error("Error fetching teams stats:", error); throw new Error("Failed to fetch teams statistics"); + } finally { + client.release(); } } @@ -241,10 +251,15 @@ export class AdminService { skills: any[]; }> { const pool = getPool(); + const client = await pool.connect(); try { + await client.query("BEGIN"); + 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(), ]); @@ -254,13 +269,18 @@ export class AdminService { skills: [], })); + await client.query("COMMIT"); + return { skillCategories, skills, }; } catch (error) { + await client.query("ROLLBACK"); console.error("Error fetching skills page data:", error); throw new Error("Failed to fetch skills page data"); + } finally { + client.release(); } } @@ -308,13 +328,16 @@ export class AdminService { users: any[]; }> { const pool = getPool(); + const client = await pool.connect(); try { + await client.query("BEGIN"); + const [teamsResult, usersResult] = await Promise.all([ - pool.query( + client.query( "SELECT id, name, direction FROM teams ORDER BY direction, name" ), - pool.query(` + client.query(` SELECT u.uuid_id as uuid, u.first_name as "firstName", @@ -330,13 +353,18 @@ export class AdminService { `), ]); + await client.query("COMMIT"); + return { teams: teamsResult.rows, users: usersResult.rows, }; } catch (error) { + await client.query("ROLLBACK"); console.error("Error fetching users page data:", error); throw new Error("Failed to fetch users page data"); + } finally { + client.release(); } } @@ -349,10 +377,13 @@ export class AdminService { directionStats: DirectionStats[]; }> { const pool = getPool(); + const client = await pool.connect(); try { + await client.query("BEGIN"); + const [teamsResult, teamStats] = await Promise.all([ - pool.query( + client.query( "SELECT id, name, direction FROM teams ORDER BY direction, name" ), AdminService.getTeamsStats(), @@ -360,14 +391,19 @@ export class AdminService { const directionStats = AdminService.generateDirectionStats(teamStats); + await client.query("COMMIT"); + return { teams: teamsResult.rows, teamStats, directionStats, }; } catch (error) { + await client.query("ROLLBACK"); console.error("Error fetching teams page data:", error); throw new Error("Failed to fetch teams page data"); + } finally { + client.release(); } } @@ -381,13 +417,18 @@ export class AdminService { directionStats: DirectionStats[]; }> { const pool = getPool(); + const client = await pool.connect(); try { + await client.query("BEGIN"); + const [teamsResult, categoriesResult, teamStats] = await Promise.all([ - pool.query( + client.query( "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(), ]); @@ -400,6 +441,8 @@ export class AdminService { const directionStats = AdminService.generateDirectionStats(teamStats); + await client.query("COMMIT"); + return { teams, skillCategories, @@ -407,8 +450,11 @@ export class AdminService { directionStats, }; } catch (error) { + await client.query("ROLLBACK"); console.error("Error fetching overview page data:", error); throw new Error("Failed to fetch overview page data"); + } finally { + client.release(); } } } diff --git a/services/team-review-service.ts b/services/team-review-service.ts index a2e0595..abc9d7a 100644 --- a/services/team-review-service.ts +++ b/services/team-review-service.ts @@ -7,7 +7,12 @@ import { TeamMember, TeamMemberSkill, } 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 { static async getTeamReviewData(teamId: string): Promise { @@ -100,7 +105,11 @@ export class TeamReviewService { skillName: row.skill_name, category: row.category, importance: row.importance || "standard", - level: row.level as SkillLevel, + level: row.level as + | "never" + | "not-autonomous" + | "autonomous" + | "expert", canMentor: row.can_mentor || false, wantsToLearn: row.wants_to_learn || false, }; @@ -140,8 +149,14 @@ export class TeamReviewService { const teamMembers = evaluations.filter((e) => e.level).length; 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 const risk = @@ -159,7 +174,7 @@ export class TeamReviewService { category: skill.category, icon: skill.icon, importance: skill.importance || "standard", - team_members: teamMembers, + teamMembers, experts, mentors, learners, @@ -175,8 +190,8 @@ export class TeamReviewService { if (!categoriesMap.has(skill.category)) { categoriesMap.set(skill.category, { category: skill.category, - total_skills: 0, - covered_skills: 0, + totalSkills: 0, + coveredSkills: 0, experts: 0, mentors: 0, learners: 0, @@ -189,13 +204,13 @@ export class TeamReviewService { } const categoryStats = categoriesMap.get(skill.category)!; - categoryStats.total_skills++; + categoryStats.totalSkills++; const skillGap = skillGaps.find( (gap) => gap.skillId === skill.skill_id ); if (skillGap) { - if (skillGap.team_members > 0) categoryStats.covered_skills++; + if (skillGap.teamMembers > 0) categoryStats.coveredSkills++; categoryStats.experts += skillGap.experts; categoryStats.mentors += skillGap.mentors; categoryStats.learners += skillGap.learners; @@ -203,11 +218,14 @@ export class TeamReviewService { // Calculer la couverture des compétences critiques if ( skillGap.importance === "incontournable" && - skillGap.coverage > 50 + !isCoverageBelowObjective(skillGap.coverage, skillGap.importance) ) { categoryStats.criticalSkillsCoverage.incontournable++; } - if (skillGap.importance === "majeure" && skillGap.coverage > 50) { + if ( + skillGap.importance === "majeure" && + !isCoverageBelowObjective(skillGap.coverage, skillGap.importance) + ) { categoryStats.criticalSkillsCoverage.majeure++; } } @@ -219,8 +237,8 @@ export class TeamReviewService { ).map((category) => ({ ...category, coverage: - category.total_skills > 0 - ? (category.covered_skills / category.total_skills) * 100 + category.totalSkills > 0 + ? (category.coveredSkills / category.totalSkills) * 100 : 0, })); @@ -236,7 +254,14 @@ export class TeamReviewService { incontournable: (skillGaps .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( 1, skillGaps.filter((gap) => gap.importance === "incontournable") @@ -246,7 +271,14 @@ export class TeamReviewService { majeure: (skillGaps .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( 1, skillGaps.filter((gap) => gap.importance === "majeure").length @@ -298,7 +330,9 @@ export class TeamReviewService { // Analyser les gaps critiques par importance const uncoveredIncontournables = skillGaps.filter( - (gap) => gap.importance === "incontournable" && gap.coverage < 50 + (gap) => + gap.importance === "incontournable" && + isCoverageBelowObjective(gap.coverage, gap.importance) ); if (uncoveredIncontournables.length > 0) { recommendations.push( @@ -311,7 +345,9 @@ export class TeamReviewService { } const uncoveredMajeures = skillGaps.filter( - (gap) => gap.importance === "majeure" && gap.coverage < 30 + (gap) => + gap.importance === "majeure" && + isCoverageBelowObjective(gap.coverage, gap.importance) ); if (uncoveredMajeures.length > 0) { recommendations.push( @@ -338,7 +374,7 @@ export class TeamReviewService { // Analyser la couverture des catégories const lowCoverageCategories = categoryCoverage - .filter((cat) => cat.coverage < 50) + .filter((cat) => cat.coverage < COVERAGE_OBJECTIVES.majeure) .map((cat) => cat.category); if (lowCoverageCategories.length > 0) { recommendations.push(