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, s.importance, 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, s.importance, 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, importance: row.importance || "standard", 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; // Déterminer le niveau de risque en fonction de l'importance const risk = skill.importance === "incontournable" && experts === 0 ? "high" : skill.importance === "majeure" && experts === 0 && mentors === 0 ? "high" : experts === 0 && mentors === 0 ? "medium" : "low"; return { skillId: skill.skill_id, skillName: skill.skill_name, category: skill.category, icon: skill.icon, importance: skill.importance || "standard", team_members: teamMembers, experts, mentors, learners, coverage, risk, }; }); // 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, criticalSkillsCoverage: { incontournable: 0, majeure: 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 des compétences critiques if ( skillGap.importance === "incontournable" && skillGap.coverage > 50 ) { categoryStats.criticalSkillsCoverage.incontournable++; } if (skillGap.importance === "majeure" && skillGap.coverage > 50) { categoryStats.criticalSkillsCoverage.majeure++; } } } // 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 criticalSkillsCoverage = { incontournable: (skillGaps .filter((gap) => gap.importance === "incontournable") .reduce((acc, gap) => acc + (gap.coverage > 50 ? 1 : 0), 0) / Math.max( 1, skillGaps.filter((gap) => gap.importance === "incontournable") .length )) * 100, majeure: (skillGaps .filter((gap) => gap.importance === "majeure") .reduce((acc, gap) => acc + (gap.coverage > 50 ? 1 : 0), 0) / Math.max( 1, skillGaps.filter((gap) => gap.importance === "majeure").length )) * 100, }; 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, criticalSkillsCoverage, }; 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 par importance const uncoveredIncontournables = skillGaps.filter( (gap) => gap.importance === "incontournable" && gap.coverage < 50 ); if (uncoveredIncontournables.length > 0) { recommendations.push( `⚠️ ${ uncoveredIncontournables.length } compétences incontournables faiblement couvertes : ${uncoveredIncontournables .map((gap) => gap.skillName) .join(", ")}` ); } const uncoveredMajeures = skillGaps.filter( (gap) => gap.importance === "majeure" && gap.coverage < 30 ); if (uncoveredMajeures.length > 0) { recommendations.push( `⚠️ ${ uncoveredMajeures.length } compétences majeures faiblement couvertes : ${uncoveredMajeures .map((gap) => gap.skillName) .join(", ")}` ); } // Analyser les opportunités de mentorat pour les compétences importantes const criticalMentorshipNeeds = skillGaps.filter( (gap) => gap.learners > 0 && gap.mentors > 0 && (gap.importance === "incontournable" || gap.importance === "majeure") ).length; if (criticalMentorshipNeeds > 0) { recommendations.push( `${criticalMentorshipNeeds} opportunités de mentorat sur des compétences critiques 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 sur des compétences importantes const isolatedExperts = members.filter((member) => member.skills.some( (skill) => skill.level === "expert" && !skill.canMentor && (skill.importance === "incontournable" || skill.importance === "majeure") ) ); if (isolatedExperts.length > 0) { recommendations.push( `${isolatedExperts.length} experts en compétences critiques pourraient devenir mentors` ); } return recommendations; } }