feat: handling SSR and PG datas for admin space
This commit is contained in:
260
services/admin-service.ts
Normal file
260
services/admin-service.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,8 @@ export { TeamsService } from "./teams-service";
|
||||
// Skills services (server-only)
|
||||
export { SkillsService } from "./skills-service";
|
||||
|
||||
// Admin services (server-only)
|
||||
export { AdminService } from "./admin-service";
|
||||
|
||||
// API client (can be used client-side)
|
||||
export { ApiClient, apiClient } from "./api-client";
|
||||
|
||||
Reference in New Issue
Block a user