import { getPool } from "./database"; import { Team, SkillCategory } from "@/lib/types"; export interface TeamMember { uuid: string; firstName: string; lastName: string; skills: Array<{ skillId: string; skillName: string; category: string; level: number; canMentor: boolean; wantsToLearn: boolean; }>; joinDate: string; } export interface TeamStats { teamId: string; teamName: string; direction: string; totalMembers: number; averageSkillLevel: number; topSkills: Array<{ skillName: string; averageLevel: number; icon?: string }>; skillCoverage: number; // Percentage of skills evaluated members: TeamMember[]; } export interface DirectionStats { direction: string; teams: TeamStats[]; totalMembers: number; averageSkillLevel: number; topCategories: Array<{ category: string; averageLevel: number }>; } export class AdminService { /** * Récupère toutes les statistiques des équipes depuis la base de données */ static async getTeamsStats(): Promise { const pool = getPool(); try { // Récupérer toutes les équipes avec leurs membres et évaluations const query = ` WITH team_members AS ( SELECT t.id as team_id, t.name as team_name, t.direction, u.uuid_id, u.first_name, u.last_name, u.created_at as join_date FROM teams t LEFT JOIN users u ON t.id = u.team_id ), skill_stats AS ( SELECT tm.team_id, tm.uuid_id, s.id as skill_id, s.name as skill_name, sc.name as category_name, CASE WHEN se.level = 'never' THEN 0 WHEN se.level = 'not-autonomous' THEN 1 WHEN se.level = 'autonomous' THEN 2 WHEN se.level = 'expert' THEN 3 ELSE 0 END as level_numeric, se.can_mentor, se.wants_to_learn FROM team_members tm LEFT JOIN user_evaluations ue ON tm.uuid_id = ue.user_uuid LEFT JOIN skill_evaluations se ON ue.id = se.user_evaluation_id AND se.is_selected = true LEFT JOIN skills s ON se.skill_id = s.id LEFT JOIN skill_categories sc ON s.category_id = sc.id WHERE tm.uuid_id IS NOT NULL ), team_skill_averages AS ( SELECT ss.team_id, ss.skill_name, s.icon as skill_icon, AVG(ss.level_numeric) as avg_level FROM skill_stats ss JOIN skills s ON ss.skill_id = s.id WHERE ss.skill_name IS NOT NULL GROUP BY ss.team_id, ss.skill_name, s.icon ) SELECT tm.team_id, tm.team_name, tm.direction, json_agg( DISTINCT jsonb_build_object( 'uuid', tm.uuid_id, 'firstName', tm.first_name, 'lastName', tm.last_name, 'joinDate', tm.join_date, 'skills', COALESCE( (SELECT json_agg( jsonb_build_object( 'skillId', ss.skill_id, 'skillName', ss.skill_name, 'category', ss.category_name, 'level', ss.level_numeric, 'canMentor', ss.can_mentor, 'wantsToLearn', ss.wants_to_learn ) ) FROM skill_stats ss WHERE ss.team_id = tm.team_id AND ss.uuid_id = tm.uuid_id AND ss.skill_name IS NOT NULL ), '[]'::json ) ) ) FILTER (WHERE tm.uuid_id IS NOT NULL) as members, COUNT(DISTINCT tm.uuid_id) FILTER (WHERE tm.uuid_id IS NOT NULL) as total_members, COALESCE(AVG(ss.level_numeric) FILTER (WHERE ss.skill_name IS NOT NULL), 0) as avg_skill_level, (SELECT json_agg( jsonb_build_object( 'skillName', tsa.skill_name, 'averageLevel', tsa.avg_level, 'icon', tsa.skill_icon ) ORDER BY tsa.avg_level DESC ) FROM team_skill_averages tsa WHERE tsa.team_id = tm.team_id LIMIT 3 ) as top_skills, CASE WHEN COUNT(DISTINCT ss.skill_id) > 0 THEN (COUNT(DISTINCT ss.skill_id) * 100.0 / (SELECT COUNT(*) FROM skills)) ELSE 0 END as skill_coverage FROM team_members tm LEFT JOIN skill_stats ss ON tm.team_id = ss.team_id AND tm.uuid_id = ss.uuid_id GROUP BY tm.team_id, tm.team_name, tm.direction ORDER BY tm.direction, tm.team_name `; const result = await pool.query(query); return result.rows.map((row) => ({ teamId: row.team_id, teamName: row.team_name, direction: row.direction, totalMembers: parseInt(row.total_members) || 0, averageSkillLevel: parseFloat(row.avg_skill_level) || 0, topSkills: row.top_skills || [], skillCoverage: parseFloat(row.skill_coverage) || 0, members: (row.members || []).filter( (member: any) => member.uuid !== null ), })); } catch (error) { console.error("Error fetching teams stats:", error); throw new Error("Failed to fetch teams statistics"); } } /** * Génère les statistiques par direction à partir des stats d'équipes */ static generateDirectionStats(teamStats: TeamStats[]): DirectionStats[] { // Filtrer d'abord les équipes avec 0 membres const teamsWithMembers = teamStats.filter((t) => t.totalMembers > 0); const directions = Array.from( new Set(teamsWithMembers.map((t) => t.direction)) ); return directions.map((direction) => { const directionTeams = teamsWithMembers.filter( (t) => t.direction === direction ); const totalMembers = directionTeams.reduce( (sum, t) => sum + t.totalMembers, 0 ); const averageSkillLevel = directionTeams.length > 0 ? directionTeams.reduce((sum, t) => sum + t.averageSkillLevel, 0) / directionTeams.length : 0; // Calculer les top catégories pour cette direction const categoryMap = new Map(); directionTeams.forEach((team) => { team.members.forEach((member) => { member.skills.forEach((skill) => { const current = categoryMap.get(skill.category) || { total: 0, count: 0, }; categoryMap.set(skill.category, { total: current.total + skill.level, count: current.count + 1, }); }); }); }); const topCategories = Array.from(categoryMap.entries()) .map(([category, stats]) => ({ category, averageLevel: stats.count > 0 ? stats.total / stats.count : 0, })) .sort((a, b) => b.averageLevel - a.averageLevel) .slice(0, 3); return { direction, teams: directionTeams, totalMembers, averageSkillLevel, topCategories, }; }); } /** * Récupère toutes les données nécessaires pour l'admin */ static async getAdminData(): Promise<{ teams: Team[]; skillCategories: SkillCategory[]; teamStats: TeamStats[]; directionStats: DirectionStats[]; }> { const pool = getPool(); try { // Récupérer toutes les données en parallèle const [teamsResult, categoriesResult, teamStats] = await Promise.all([ pool.query( "SELECT id, name, direction FROM teams ORDER BY direction, name" ), pool.query("SELECT id, name, icon FROM skill_categories ORDER BY name"), AdminService.getTeamsStats(), ]); const teams = teamsResult.rows; const skillCategories = categoriesResult.rows.map((row) => ({ ...row, category: row.name, // Adapter le format skills: [], // Les skills individuelles ne sont pas nécessaires pour l'admin })); const directionStats = AdminService.generateDirectionStats(teamStats); return { teams, skillCategories, teamStats, directionStats, }; } catch (error) { console.error("Error fetching admin data:", error); throw new Error("Failed to fetch admin data"); } } }