299 lines
9.0 KiB
TypeScript
299 lines
9.0 KiB
TypeScript
import { getPool } from "./database";
|
|
import {
|
|
TeamReviewData,
|
|
TeamMemberProfile,
|
|
SkillGap,
|
|
CategoryCoverage,
|
|
TeamMember,
|
|
TeamMemberSkill,
|
|
} from "@/lib/team-review-types";
|
|
import { SkillLevel } from "@/lib/types";
|
|
|
|
export class TeamReviewService {
|
|
static async getTeamReviewData(teamId: string): Promise<TeamReviewData> {
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
await client.query("BEGIN");
|
|
|
|
// 1. Récupérer les infos de l'équipe
|
|
const teamQuery = `
|
|
SELECT id, name, direction
|
|
FROM teams
|
|
WHERE id = $1
|
|
`;
|
|
const teamResult = await client.query(teamQuery, [teamId]);
|
|
const team = teamResult.rows[0];
|
|
|
|
// 2. Récupérer les compétences qui ont au moins une évaluation dans l'équipe
|
|
const skillsQuery = `
|
|
SELECT DISTINCT
|
|
s.id as skill_id,
|
|
s.name as skill_name,
|
|
s.icon,
|
|
sc.name as category
|
|
FROM skill_categories sc
|
|
JOIN skills s ON s.category_id = sc.id
|
|
JOIN skill_evaluations se ON s.id = se.skill_id
|
|
JOIN user_evaluations ue ON se.user_evaluation_id = ue.id
|
|
JOIN users u ON ue.user_uuid = u.uuid_id
|
|
WHERE u.team_id = $1
|
|
ORDER BY sc.name, s.name
|
|
`;
|
|
const skillsResult = await client.query(skillsQuery, [teamId]);
|
|
const allSkills = skillsResult.rows;
|
|
|
|
// 3. Récupérer les évaluations des membres
|
|
const membersQuery = `
|
|
SELECT
|
|
u.uuid_id,
|
|
u.first_name,
|
|
u.last_name,
|
|
u.team_id,
|
|
u.email,
|
|
s.id as skill_id,
|
|
s.name as skill_name,
|
|
sc.name as category,
|
|
se.level,
|
|
se.can_mentor,
|
|
se.wants_to_learn
|
|
FROM users u
|
|
LEFT JOIN user_evaluations ue ON u.uuid_id = ue.user_uuid
|
|
LEFT JOIN skill_evaluations se ON ue.id = se.user_evaluation_id
|
|
LEFT JOIN skills s ON se.skill_id = s.id
|
|
LEFT JOIN skill_categories sc ON s.category_id = sc.id
|
|
WHERE u.team_id = $1
|
|
ORDER BY u.first_name, u.last_name, sc.name, s.name
|
|
`;
|
|
const membersResult = await client.query(membersQuery, [teamId]);
|
|
|
|
// 4. Organiser les données des membres
|
|
const membersMap = new Map<string, TeamMemberProfile>();
|
|
|
|
for (const row of membersResult.rows) {
|
|
if (!membersMap.has(row.uuid_id)) {
|
|
membersMap.set(row.uuid_id, {
|
|
member: {
|
|
uuid: row.uuid_id,
|
|
firstName: row.first_name,
|
|
lastName: row.last_name,
|
|
teamId: row.team_id,
|
|
email: row.email,
|
|
},
|
|
skills: [],
|
|
totalSkills: 0,
|
|
expertSkills: 0,
|
|
mentorSkills: 0,
|
|
learningSkills: 0,
|
|
averageLevel: 0,
|
|
});
|
|
}
|
|
|
|
const profile = membersMap.get(row.uuid_id)!;
|
|
|
|
if (row.skill_id && row.skill_name && row.category) {
|
|
const skill: TeamMemberSkill = {
|
|
skillId: row.skill_id,
|
|
skillName: row.skill_name,
|
|
category: row.category,
|
|
level: row.level as SkillLevel,
|
|
canMentor: row.can_mentor || false,
|
|
wantsToLearn: row.wants_to_learn || false,
|
|
};
|
|
|
|
profile.skills.push(skill);
|
|
profile.totalSkills++;
|
|
|
|
if (row.level === "expert") profile.expertSkills++;
|
|
if (row.can_mentor) profile.mentorSkills++;
|
|
if (row.wants_to_learn) profile.learningSkills++;
|
|
}
|
|
}
|
|
|
|
// Calculer les moyennes
|
|
for (const profile of membersMap.values()) {
|
|
profile.averageLevel =
|
|
profile.skills.reduce((acc, skill) => {
|
|
const levelValues = {
|
|
never: 0,
|
|
"not-autonomous": 1,
|
|
autonomous: 2,
|
|
expert: 3,
|
|
};
|
|
return acc + (levelValues[skill.level] || 0);
|
|
}, 0) / (profile.skills.length || 1);
|
|
}
|
|
|
|
// 5. Analyser les gaps de compétences
|
|
const skillGaps: SkillGap[] = allSkills.map((skill) => {
|
|
const evaluations = membersResult.rows.filter(
|
|
(row) => row.skill_id === skill.skill_id
|
|
);
|
|
|
|
const experts = evaluations.filter((e) => e.level === "expert").length;
|
|
const mentors = evaluations.filter((e) => e.can_mentor).length;
|
|
const learners = evaluations.filter((e) => e.wants_to_learn).length;
|
|
const teamMembers = evaluations.filter((e) => e.level).length;
|
|
|
|
const totalTeamMembers = membersMap.size;
|
|
const coverage =
|
|
totalTeamMembers > 0 ? (teamMembers / totalTeamMembers) * 100 : 0;
|
|
|
|
return {
|
|
skillId: skill.skill_id,
|
|
skillName: skill.skill_name,
|
|
category: skill.category,
|
|
icon: skill.icon,
|
|
team_members: teamMembers,
|
|
experts,
|
|
mentors,
|
|
learners,
|
|
coverage,
|
|
risk:
|
|
experts === 0 && mentors === 0
|
|
? "high"
|
|
: experts === 0 || mentors === 0
|
|
? "medium"
|
|
: "low",
|
|
};
|
|
});
|
|
|
|
// 6. Analyser la couverture par catégorie
|
|
const categoriesMap = new Map<string, CategoryCoverage>();
|
|
|
|
for (const skill of allSkills) {
|
|
if (!categoriesMap.has(skill.category)) {
|
|
categoriesMap.set(skill.category, {
|
|
category: skill.category,
|
|
total_skills: 0,
|
|
covered_skills: 0,
|
|
experts: 0,
|
|
mentors: 0,
|
|
learners: 0,
|
|
coverage: 0,
|
|
});
|
|
}
|
|
|
|
const categoryStats = categoriesMap.get(skill.category)!;
|
|
categoryStats.total_skills++;
|
|
|
|
const skillGap = skillGaps.find(
|
|
(gap) => gap.skillId === skill.skill_id
|
|
);
|
|
if (skillGap) {
|
|
if (skillGap.team_members > 0) categoryStats.covered_skills++;
|
|
categoryStats.experts += skillGap.experts;
|
|
categoryStats.mentors += skillGap.mentors;
|
|
categoryStats.learners += skillGap.learners;
|
|
}
|
|
}
|
|
|
|
// Calculer la couverture pour chaque catégorie
|
|
const categoryCoverage: CategoryCoverage[] = Array.from(
|
|
categoriesMap.values()
|
|
).map((category) => ({
|
|
...category,
|
|
coverage:
|
|
category.total_skills > 0
|
|
? (category.covered_skills / category.total_skills) * 100
|
|
: 0,
|
|
}));
|
|
|
|
// 7. Générer des recommandations
|
|
const recommendations = this.generateRecommendations(
|
|
Array.from(membersMap.values()),
|
|
skillGaps,
|
|
categoryCoverage
|
|
);
|
|
|
|
// 8. Calculer les statistiques globales
|
|
const stats = {
|
|
totalMembers: membersMap.size,
|
|
totalSkills: skillGaps.length,
|
|
averageTeamLevel:
|
|
Array.from(membersMap.values()).reduce(
|
|
(acc, profile) => acc + profile.averageLevel,
|
|
0
|
|
) / (membersMap.size || 1),
|
|
mentorshipOpportunities: skillGaps.reduce(
|
|
(acc, gap) => acc + (gap.learners || 0),
|
|
0
|
|
),
|
|
learningNeeds: skillGaps.filter((gap) => gap.risk === "high").length,
|
|
};
|
|
|
|
await client.query("COMMIT");
|
|
|
|
return {
|
|
team,
|
|
members: Array.from(membersMap.values()),
|
|
skillGaps,
|
|
categoryCoverage,
|
|
recommendations,
|
|
stats,
|
|
};
|
|
} catch (error) {
|
|
await client.query("ROLLBACK");
|
|
console.error("Error in getTeamReviewData:", error);
|
|
throw new Error("Failed to get team review data");
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
private static generateRecommendations(
|
|
members: TeamMemberProfile[],
|
|
skillGaps: SkillGap[],
|
|
categoryCoverage: CategoryCoverage[]
|
|
): string[] {
|
|
const recommendations: string[] = [];
|
|
|
|
// Analyser les gaps critiques
|
|
const criticalGaps = skillGaps.filter((gap) => gap.risk === "high");
|
|
if (criticalGaps.length > 0) {
|
|
recommendations.push(
|
|
`Attention : ${
|
|
criticalGaps.length
|
|
} compétences critiques sans expert ni mentor : ${criticalGaps
|
|
.map((gap) => gap.skillName)
|
|
.join(", ")}`
|
|
);
|
|
}
|
|
|
|
// Analyser les opportunités de mentorat
|
|
const mentorshipNeeds = skillGaps.filter(
|
|
(gap) => gap.learners > 0 && gap.mentors > 0
|
|
).length;
|
|
if (mentorshipNeeds > 0) {
|
|
recommendations.push(
|
|
`${mentorshipNeeds} opportunités de mentorat identifiées`
|
|
);
|
|
}
|
|
|
|
// Analyser la couverture des catégories
|
|
const lowCoverageCategories = categoryCoverage
|
|
.filter((cat) => cat.coverage < 50)
|
|
.map((cat) => cat.category);
|
|
if (lowCoverageCategories.length > 0) {
|
|
recommendations.push(
|
|
`Faible couverture dans les catégories : ${lowCoverageCategories.join(
|
|
", "
|
|
)}`
|
|
);
|
|
}
|
|
|
|
// Identifier les experts isolés
|
|
const isolatedExperts = members.filter(
|
|
(member) => member.expertSkills > 0 && member.mentorSkills === 0
|
|
);
|
|
if (isolatedExperts.length > 0) {
|
|
recommendations.push(
|
|
`${isolatedExperts.length} experts pourraient devenir mentors`
|
|
);
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
}
|