261 lines
8.2 KiB
TypeScript
261 lines
8.2 KiB
TypeScript
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<TeamStats[]> {
|
|
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[] {
|
|
const directions = Array.from(new Set(teamStats.map((t) => t.direction)));
|
|
|
|
return directions.map((direction) => {
|
|
const directionTeams = teamStats.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<string, { total: number; count: number }>();
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|