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 { 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(); 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(); 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; } }