feat: my team page

This commit is contained in:
Julien Froidefond
2025-08-27 10:53:11 +02:00
parent b7e6fa257e
commit c7a5b25501
11 changed files with 1529 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
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;
}
}