refactor: rule of coverage are in one place
This commit is contained in:
@@ -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<TeamStats[]> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TeamReviewData> {
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user