Files
peakskills/services/admin-service.ts
2025-08-25 09:20:36 +02:00

336 lines
10 KiB
TypeScript

import { getPool } from "./database";
import { Team, SkillCategory } from "@/lib/types";
import { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
import { SkillsService } from "./skills-service";
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[] {
// 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<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 les données nécessaires pour la page de gestion des skills
*/
static async getSkillsPageData(): Promise<{
skillCategories: SkillCategory[];
skills: any[];
}> {
const pool = getPool();
try {
const [categoriesResult, skills] = await Promise.all([
pool.query("SELECT id, name, icon FROM skill_categories ORDER BY name"),
SkillsService.getAllSkillsWithUsage(),
]);
const skillCategories = categoriesResult.rows.map((row) => ({
...row,
category: row.name,
skills: [],
}));
return {
skillCategories,
skills,
};
} catch (error) {
console.error("Error fetching skills page data:", error);
throw new Error("Failed to fetch skills page data");
}
}
/**
* Récupère les données nécessaires pour la page de gestion des utilisateurs
*/
static async getUsersPageData(): Promise<{
teams: Team[];
users: any[];
}> {
const pool = getPool();
try {
const [teamsResult, usersResult] = await Promise.all([
pool.query(
"SELECT id, name, direction FROM teams ORDER BY direction, name"
),
pool.query(`
SELECT
u.uuid_id as uuid,
u.first_name as "firstName",
u.last_name as "lastName",
t.name as "teamName",
EXISTS (
SELECT 1 FROM user_evaluations ue
WHERE ue.user_uuid = u.uuid_id
) as "hasEvaluations"
FROM users u
LEFT JOIN teams t ON u.team_id = t.id
ORDER BY u.first_name, u.last_name
`),
]);
return {
teams: teamsResult.rows,
users: usersResult.rows,
};
} catch (error) {
console.error("Error fetching users page data:", error);
throw new Error("Failed to fetch users page data");
}
}
/**
* Récupère les données nécessaires pour la page de gestion des équipes
*/
static async getTeamsPageData(): Promise<{
teams: Team[];
teamStats: TeamStats[];
directionStats: DirectionStats[];
}> {
const pool = getPool();
try {
const [teamsResult, teamStats] = await Promise.all([
pool.query(
"SELECT id, name, direction FROM teams ORDER BY direction, name"
),
AdminService.getTeamsStats(),
]);
const directionStats = AdminService.generateDirectionStats(teamStats);
return {
teams: teamsResult.rows,
teamStats,
directionStats,
};
} catch (error) {
console.error("Error fetching teams page data:", error);
throw new Error("Failed to fetch teams page data");
}
}
/**
* Récupère les données nécessaires pour la page d'accueil admin
*/
static async getOverviewPageData(): Promise<{
teams: Team[];
skillCategories: SkillCategory[];
teamStats: TeamStats[];
directionStats: DirectionStats[];
}> {
const pool = getPool();
try {
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,
skills: [],
}));
const directionStats = AdminService.generateDirectionStats(teamStats);
return {
teams,
skillCategories,
teamStats,
directionStats,
};
} catch (error) {
console.error("Error fetching overview page data:", error);
throw new Error("Failed to fetch overview page data");
}
}
}