-
- Chargement des données d'administration...
+
+ Erreur lors du chargement des données d'administration
);
}
-
- return (
-
- {/* Background Effects */}
-
-
-
-
-
- {/* Header */}
-
-
-
-
- Administration
-
-
-
-
- Dashboard Managérial
-
-
-
- Vue d'ensemble des compétences par équipe et direction pour pilotage
- stratégique
-
-
-
- {/* Overview Cards */}
-
-
-
-
-
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? "FILTRÉES"
- : "TOTAL"}
-
-
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? getFilteredTeamStats().length
- : teams.length}
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? "Équipes filtrées"
- : "Équipes"}
-
- {(selectedDirections.length > 0 || selectedTeams.length > 0) && (
-
- sur {teams.length} au total
-
- )}
-
-
-
-
-
-
-
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? "FILTRÉS"
- : "TOTAL"}
-
-
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? getFilteredTeamStats().reduce(
- (sum, t) => sum + t.totalMembers,
- 0
- )
- : teamStats.reduce((sum, t) => sum + t.totalMembers, 0)}
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? "Membres filtrés"
- : "Membres"}
-
- {(selectedDirections.length > 0 || selectedTeams.length > 0) && (
-
- sur {teamStats.reduce((sum, t) => sum + t.totalMembers, 0)} au
- total
-
- )}
-
-
-
-
-
-
-
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? "FILTRÉES"
- : "TOTAL"}
-
-
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? getFilteredDirectionStats().length
- : directionStats.length}
-
-
- {selectedDirections.length > 0 || selectedTeams.length > 0
- ? "Directions filtrées"
- : "Directions"}
-
- {(selectedDirections.length > 0 || selectedTeams.length > 0) && (
-
- sur {directionStats.length} au total
-
- )}
-
-
-
-
-
-
-
-
-
- RÉFÉRENTIEL
-
-
-
-
- {skillCategories.reduce(
- (sum, cat) => sum + cat.skills.length,
- 0
- )}
-
-
Compétences suivies
-
- {skillCategories.length} catégories
-
-
-
-
-
- {/* Filtres */}
-
-
-
-
-
-
- Filtres avancés
-
-
-
-
-
- }
- />
-
-
- }
- />
-
-
- {/* Résumé des filtres actifs */}
- {(selectedDirections.length > 0 || selectedTeams.length > 0) && (
-
-
-
-
-
- {getFilteredTeamStats().reduce(
- (sum, t) => sum + t.totalMembers,
- 0
- )}{" "}
- membres
-
-
-
-
{
- setSelectedDirections([]);
- setSelectedTeams([]);
- }}
- className="text-xs text-slate-400 hover:text-white hover:bg-white/10"
- >
- Réinitialiser
-
-
- )}
-
-
-
- {/* Main Content Tabs */}
-
-
-
-
- Vue par Direction
-
-
- Vue par Équipe
-
-
-
-
-
-
- {getFilteredDirectionStats().length > 0 ? (
- getFilteredDirectionStats().map((direction) => (
-
- ))
- ) : (
-
-
-
-
-
- Aucune direction trouvée
-
-
- Aucune direction ne correspond aux filtres sélectionnés.
-
-
- )}
-
-
-
-
-
- {getFilteredTeamStats().length > 0 ? (
- getFilteredTeamStats().map((team) => (
-
handleViewTeamDetails(team)}
- onViewReport={() => handleExportTeamReport(team)}
- />
- ))
- ) : (
-
-
-
-
-
-
- Aucune équipe trouvée
-
-
- Aucune équipe ne correspond aux filtres sélectionnés.
-
-
-
- )}
-
-
-
-
- {/* Modale de détails d'équipe */}
-
setIsModalOpen(false)}
- team={selectedTeamForModal}
- />
-
-
- );
}
diff --git a/app/admin/team/[teamId]/page.tsx b/app/admin/team/[teamId]/page.tsx
index c8fd1ce..e48ab04 100644
--- a/app/admin/team/[teamId]/page.tsx
+++ b/app/admin/team/[teamId]/page.tsx
@@ -1,950 +1,45 @@
-"use client";
+import { redirect } from "next/navigation";
+import { isUserAuthenticated } from "@/lib/server-auth";
+import { AdminService, TeamStats } from "@/services/admin-service";
+import { TeamDetailClientWrapper } from "@/components/admin";
-import React, { useState, useEffect } from "react";
-import { useParams, useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import {
- Users,
- TrendingUp,
- Target,
- Award,
- BarChart3,
- ArrowLeft,
- Download,
- UserCheck,
- GraduationCap,
- Building2,
- Star,
- BookOpen,
- X,
- ChevronRight,
-} from "lucide-react";
-import { TechIcon } from "@/components/icons/tech-icon";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { loadTeams, loadSkillCategories } from "@/lib/data-loader";
-import { Team, SkillCategory } from "@/lib/types";
-
-interface TeamMember {
- id: string;
- firstName: string;
- lastName: string;
- skills: Array<{
- skillId: string;
- skillName: string;
- category: string;
- level: number;
- canMentor: boolean;
- wantsToLearn: boolean;
- }>;
- joinDate: string;
+interface TeamDetailPageProps {
+ params: {
+ teamId: string;
+ };
}
-interface TeamStats {
- teamId: string;
- teamName: string;
- direction: string;
- totalMembers: number;
- averageSkillLevel: number;
- topSkills: Array<{ skillName: string; averageLevel: number }>;
- skillCoverage: number;
- members: TeamMember[];
-}
+export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
+ // Vérifier l'authentification
+ const isAuthenticated = await isUserAuthenticated();
-function getSkillLevelLabel(level: number): string {
- if (level < 0.5) return "Débutant";
- if (level < 1.5) return "Intermé.";
- if (level < 2.5) return "Avancé";
- return "Expert";
-}
+ // Si pas de cookie d'authentification, rediriger vers login
+ if (!isAuthenticated) {
+ redirect("/login");
+ }
-function getSkillLevelColor(level: number): string {
- if (level < 0.5) return "bg-red-500";
- if (level < 1.5) return "bg-orange-500";
- if (level < 2.5) return "bg-blue-500";
- return "bg-green-500";
-}
+ try {
+ // Charger les données côté serveur
+ const allTeamsStats = await AdminService.getTeamsStats();
+ const foundTeamStats = allTeamsStats.find(
+ (t) => t.teamId === params.teamId
+ );
-// Mock data generation (same as admin page)
-function generateTeamMembers(
- teamName: string,
- memberCount: number,
- skillCategories: SkillCategory[]
-) {
- const firstNames = [
- "Alice",
- "Bob",
- "Claire",
- "David",
- "Emma",
- "Frank",
- "Grace",
- "Hugo",
- "Iris",
- "Jack",
- "Kelly",
- "Liam",
- "Maya",
- "Noah",
- "Olivia",
- "Paul",
- "Quinn",
- "Rose",
- "Sam",
- "Tara",
- ];
- const lastNames = [
- "Martin",
- "Bernard",
- "Dubois",
- "Thomas",
- "Robert",
- "Richard",
- "Petit",
- "Durand",
- "Leroy",
- "Moreau",
- "Simon",
- "Laurent",
- "Michel",
- "Garcia",
- "David",
- "Bertrand",
- "Roux",
- "Vincent",
- "Fournier",
- "Morel",
- ];
-
- const allSkills: Array<{ skillName: string; category: string; id: string }> =
- [];
- skillCategories.forEach((category) => {
- category.skills.forEach((skill) => {
- allSkills.push({
- skillName: skill.name,
- category: category.category,
- id: skill.id,
- });
- });
- });
-
- return Array.from({ length: memberCount }, (_, index) => {
- const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
- const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
-
- const skillCount = Math.floor(Math.random() * 8) + 8;
- const memberSkills = allSkills
- .sort(() => Math.random() - 0.5)
- .slice(0, skillCount)
- .map((skill) => ({
- skillId: skill.id,
- skillName: skill.skillName,
- category: skill.category,
- level: Math.floor(Math.random() * 4),
- canMentor: Math.random() > 0.7,
- wantsToLearn: Math.random() > 0.6,
- }));
-
- return {
- id: `${teamName}-member-${index + 1}`,
- firstName,
- lastName,
- skills: memberSkills,
- joinDate: new Date(
- Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000 * 3
- )
- .toISOString()
- .split("T")[0],
- };
- });
-}
-
-function getRandomSkills(skillCategories: SkillCategory[], count: number = 3) {
- const allSkills: Array<{ skillName: string; category: string }> = [];
- skillCategories.forEach((category) => {
- category.skills.forEach((skill) => {
- allSkills.push({ skillName: skill.name, category: category.category });
- });
- });
-
- const shuffled = allSkills.sort(() => Math.random() - 0.5);
- const randomSkills = shuffled.slice(0, count).map((skill) => ({
- skillName: skill.skillName,
- averageLevel: Math.random() * 3,
- }));
-
- return randomSkills.sort((a, b) => b.averageLevel - a.averageLevel);
-}
-
-export default function TeamDetailPage() {
- const params = useParams();
- const router = useRouter();
- const teamId = params.teamId as string;
-
- const [team, setTeam] = useState
(null);
- const [loading, setLoading] = useState(true);
- const [skillAnalysis, setSkillAnalysis] = useState([]);
- const [selectedMember, setSelectedMember] = useState(null);
- const [isMemberModalOpen, setIsMemberModalOpen] = useState(false);
-
- useEffect(() => {
- async function loadTeamData() {
- try {
- const [teamsData, categoriesData] = await Promise.all([
- loadTeams(),
- loadSkillCategories(),
- ]);
-
- const foundTeam = teamsData.find((t) => t.id === teamId);
- if (!foundTeam) {
- router.push("/admin");
- return;
- }
-
- const memberCount = Math.floor(Math.random() * 10) + 3;
- const members = generateTeamMembers(
- foundTeam.name,
- memberCount,
- categoriesData
- );
-
- const teamStats: TeamStats = {
- teamId: foundTeam.id,
- teamName: foundTeam.name,
- direction: foundTeam.direction,
- totalMembers: memberCount,
- averageSkillLevel: Math.random() * 3,
- topSkills: getRandomSkills(categoriesData, 3),
- skillCoverage: Math.random() * 100,
- members: members,
- };
-
- setTeam(teamStats);
-
- // Analyser les compétences
- const skillMap = new Map();
- members.forEach((member) => {
- member.skills.forEach((skill) => {
- if (!skillMap.has(skill.skillName)) {
- // Trouver l'icône de la compétence dans les données
- let skillIcon = null;
- for (const category of categoriesData) {
- const foundSkill = category.skills.find(
- (s) => s.name === skill.skillName
- );
- if (foundSkill) {
- skillIcon = foundSkill.icon;
- break;
- }
- }
-
- skillMap.set(skill.skillName, {
- skillName: skill.skillName,
- category: skill.category,
- icon: skillIcon,
- experts: [],
- learners: [],
- averageLevel: 0,
- totalEvaluations: 0,
- });
- }
-
- const skillData = skillMap.get(skill.skillName);
- skillData.totalEvaluations++;
- skillData.averageLevel += skill.level;
-
- if (skill.level >= 2) {
- skillData.experts.push({
- name: `${member.firstName} ${member.lastName}`,
- level: skill.level,
- canMentor: skill.canMentor,
- });
- }
-
- if (skill.wantsToLearn) {
- skillData.learners.push({
- name: `${member.firstName} ${member.lastName}`,
- currentLevel: skill.level,
- });
- }
- });
- });
-
- const skills = Array.from(skillMap.values())
- .map((skill) => ({
- ...skill,
- averageLevel: skill.averageLevel / skill.totalEvaluations,
- }))
- .sort((a, b) => b.averageLevel - a.averageLevel);
-
- setSkillAnalysis(skills);
- } catch (error) {
- console.error("Error loading team data:", error);
- router.push("/admin");
- } finally {
- setLoading(false);
- }
+ if (!foundTeamStats) {
+ redirect("/admin");
}
- loadTeamData();
- }, [teamId, router]);
-
- const handleMemberClick = (member: TeamMember) => {
- setSelectedMember(member);
- setIsMemberModalOpen(true);
- };
-
- const handleCloseMemberModal = () => {
- setIsMemberModalOpen(false);
- setSelectedMember(null);
- };
-
- const handleExportReport = () => {
- if (!team) return;
-
- const reportData = {
- team: team.teamName,
- direction: team.direction,
- exportDate: new Date().toLocaleDateString(),
- summary: {
- totalMembers: team.totalMembers,
- averageSkillLevel: team.averageSkillLevel,
- skillCoverage: team.skillCoverage,
- },
- topSkills: team.topSkills,
- skillAnalysis: skillAnalysis.slice(0, 15),
- members: team.members.map((member) => ({
- name: `${member.firstName} ${member.lastName}`,
- joinDate: member.joinDate,
- skillsCount: member.skills.length,
- averageLevel:
- member.skills.reduce((sum, s) => sum + s.level, 0) /
- member.skills.length,
- skills: member.skills.map((skill) => ({
- skillName: skill.skillName,
- category: skill.category,
- level: skill.level,
- levelLabel: getSkillLevelLabel(skill.level),
- canMentor: skill.canMentor,
- wantsToLearn: skill.wantsToLearn,
- })),
- })),
- mentorshipOpportunities: skillAnalysis
- .filter(
- (skill) => skill.experts.length > 0 && skill.learners.length > 0
- )
- .map((skill) => ({
- skillName: skill.skillName,
- experts: skill.experts.filter((e) => e.canMentor),
- learners: skill.learners,
- })),
- };
-
- const dataStr = JSON.stringify(reportData, null, 2);
- const dataUri =
- "data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
-
- const exportFileDefaultName = `rapport-complet-${team.teamName.toLowerCase()}-${
- new Date().toISOString().split("T")[0]
- }.json`;
-
- const linkElement = document.createElement("a");
- linkElement.setAttribute("href", dataUri);
- linkElement.setAttribute("download", exportFileDefaultName);
- linkElement.click();
- };
-
- if (loading) {
return (
-
-
-
-
-
+
+ );
+ } catch (error) {
+ console.error("Failed to load team data:", error);
+ return (
+
+
+ Erreur lors du chargement des détails de l'équipe
);
}
-
- if (!team) {
- return (
-
- );
- }
-
- return (
-
-
-
-
-
- {/* Header */}
-
-
-
-
router.back()}
- className="text-slate-400 hover:text-white hover:bg-white/10"
- >
-
- Retour
-
-
-
-
-
-
-
- Équipe {team.teamName}
-
-
- {team.direction} • {team.totalMembers} membres
-
-
-
-
-
-
- Exporter rapport complet
-
-
-
-
- {/* Stats générales */}
-
-
-
-
- {team.totalMembers}
-
-
Membres actifs
-
-
-
-
- {((team.averageSkillLevel / 3) * 100).toFixed(0)}%
-
-
Niveau moyen
-
-
-
-
- {skillAnalysis.length}
-
-
Compétences évaluées
-
-
-
-
- {skillAnalysis.filter((s) => s.experts.length > 0).length}
-
-
Expertises disponibles
-
-
-
-
- {/* Analyse des compétences */}
-
-
-
-
-
- Top Compétences
-
-
-
- {skillAnalysis.slice(0, 15).map((skill, idx) => (
-
-
- {/* Icône de la technologie */}
-
- {skill.icon ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
- {/* Contenu principal */}
-
-
-
- {skill.skillName}
-
-
-
- {((skill.averageLevel / 3) * 100).toFixed(0)}%
-
-
-
-
-
- {/* Barre de progression */}
-
-
- {/* Métadonnées */}
-
-
- {skill.category}
-
-
- {skill.experts.length > 0 && (
-
-
-
- {skill.experts.length}
-
-
- )}
- {skill.learners.length > 0 && (
-
-
-
- {skill.learners.length}
-
-
- )}
-
-
-
-
-
- ))}
-
-
-
-
- {/* Experts et mentors */}
-
-
-
-
-
-
-
- Experts & Mentors
-
-
-
- {skillAnalysis
- .filter((skill) => skill.experts.length > 0)
- .slice(0, 20)
- .map((skill, idx) => (
-
-
- {/* Icône de la technologie */}
-
- {skill.icon ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
- {/* Nom de la compétence */}
-
-
- {skill.skillName}
-
-
- {skill.category}
-
-
-
- {/* Badge du nombre d'experts */}
-
-
-
- {skill.experts.length} expert
- {skill.experts.length > 1 ? "s" : ""}
-
-
-
-
-
- {/* Liste des experts */}
-
- {skill.experts.map((expert, expertIdx) => (
-
-
-
-
- {expert.name
- .split(" ")
- .map((n) => n[0])
- .join("")}
-
-
-
- {expert.name}
-
-
-
-
= 3
- ? "bg-green-500/20 text-green-300 border border-green-500/30"
- : expert.level >= 2
- ? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
- : "bg-orange-500/20 text-orange-300 border border-orange-500/30"
- }`}
- >
- {getSkillLevelLabel(expert.level)}
-
- {expert.canMentor && (
-
-
-
- )}
-
-
- ))}
-
-
- ))}
-
-
-
-
- {/* Liste des membres */}
-
-
-
-
-
-
-
- Membres de l'équipe
-
-
-
- {team.members.map((member, idx) => {
- const avgLevel =
- member.skills.reduce((sum, s) => sum + s.level, 0) /
- member.skills.length;
- const mentorSkills = member.skills.filter(
- (s) => s.canMentor
- ).length;
- const learningSkills = member.skills.filter(
- (s) => s.wantsToLearn
- ).length;
-
- return (
-
handleMemberClick(member)}
- className="group p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 hover:border-white/20 transition-all duration-200 cursor-pointer"
- >
-
- {/* Avatar */}
-
-
-
- {member.firstName[0]}
- {member.lastName[0]}
-
-
-
-
- {/* Informations principales */}
-
-
-
- {member.firstName} {member.lastName}
-
-
-
- {((avgLevel / 3) * 100).toFixed(0)}%
-
-
-
-
-
-
- {/* Date d'arrivée */}
-
- Membre depuis{" "}
- {new Date(member.joinDate).toLocaleDateString(
- "fr-FR",
- {
- year: "numeric",
- month: "long",
- }
- )}
-
-
- {/* Barre de progression du niveau */}
-
-
- {/* Statistiques */}
-
-
-
- {member.skills.length}
-
-
Compétences
-
-
-
- {mentorSkills}
-
-
Mentor
-
-
-
- {learningSkills}
-
-
Apprend
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
- {/* Modal des détails du membre */}
-
-
-
-
- {selectedMember && (
- <>
-
-
- {selectedMember.firstName[0]}
- {selectedMember.lastName[0]}
-
-
-
-
- {selectedMember.firstName} {selectedMember.lastName}
-
-
- {selectedMember.skills.length} compétences évaluées
-
-
- >
- )}
-
-
-
- {selectedMember && (
-
- {/* Statistiques rapides */}
-
-
-
- {selectedMember.skills.length}
-
-
Compétences
-
-
-
- {selectedMember.skills.filter((s) => s.canMentor).length}
-
-
Peut mentorer
-
-
-
- {selectedMember.skills.filter((s) => s.wantsToLearn).length}
-
-
Veut apprendre
-
-
-
- {/* Liste des compétences par catégorie */}
-
-
-
- Compétences détaillées
-
-
- {/* Grouper les compétences par catégorie */}
- {Object.entries(
- selectedMember.skills.reduce((acc, skill) => {
- if (!acc[skill.category]) {
- acc[skill.category] = [];
- }
- acc[skill.category].push(skill);
- return acc;
- }, {} as Record
)
- ).map(([category, skills]) => (
-
-
-
- {category}
-
- ({skills.length} compétence
- {skills.length > 1 ? "s" : ""})
-
-
-
-
- {skills
- .sort((a, b) => b.level - a.level)
- .map((skill, skillIdx) => {
- // Trouver l'icône de la compétence
- let skillIcon = null;
- if (skillAnalysis.length > 0) {
- const foundSkill = skillAnalysis.find(
- (s) => s.skillName === skill.skillName
- );
- if (foundSkill) {
- skillIcon = foundSkill.icon;
- }
- }
-
- return (
-
- {/* Icône de la technologie */}
-
- {skillIcon ? (
-
- ) : (
-
- )}
-
-
- {/* Nom de la compétence */}
-
-
- {skill.skillName}
-
-
-
- {/* Niveau et badges */}
-
- {/* Badge de niveau */}
-
= 3
- ? "bg-green-500/20 text-green-300"
- : skill.level >= 2
- ? "bg-blue-500/20 text-blue-300"
- : skill.level >= 1
- ? "bg-orange-500/20 text-orange-300"
- : "bg-red-500/20 text-red-300"
- }`}
- >
- {getSkillLevelLabel(skill.level)}
-
-
- {/* Icônes mentor/apprenant */}
- {skill.canMentor && (
-
-
-
- )}
- {skill.wantsToLearn && (
-
-
-
- )}
-
-
- );
- })}
-
-
- ))}
-
-
- )}
-
-
-
- );
}
diff --git a/components/admin/admin-client-wrapper.tsx b/components/admin/admin-client-wrapper.tsx
new file mode 100644
index 0000000..4390f60
--- /dev/null
+++ b/components/admin/admin-client-wrapper.tsx
@@ -0,0 +1,442 @@
+"use client";
+
+import { useState } from "react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { Users, Target, Building2, UserCheck, Filter } from "lucide-react";
+import { Team, SkillCategory } from "@/lib/types";
+import {
+ TeamStatsCard,
+ DirectionOverview,
+ MultiSelectFilter,
+} from "@/components/admin";
+import { TeamDetailModal } from "@/components/admin/team-detail-modal";
+import { TeamStats, DirectionStats } from "@/services/admin-service";
+
+interface AdminClientWrapperProps {
+ teams: Team[];
+ skillCategories: SkillCategory[];
+ teamStats: TeamStats[];
+ directionStats: DirectionStats[];
+}
+
+export function AdminClientWrapper({
+ teams,
+ skillCategories,
+ teamStats,
+ directionStats,
+}: AdminClientWrapperProps) {
+ // Filtres
+ const [selectedDirections, setSelectedDirections] = useState
([]);
+ const [selectedTeams, setSelectedTeams] = useState([]);
+
+ // Modale de détails
+ const [selectedTeamForModal, setSelectedTeamForModal] =
+ useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ // Fonctions de filtrage
+ const getFilteredTeamStats = () => {
+ let filtered = teamStats;
+
+ if (selectedDirections.length > 0) {
+ filtered = filtered.filter((team) =>
+ selectedDirections.includes(team.direction)
+ );
+ }
+
+ if (selectedTeams.length > 0) {
+ filtered = filtered.filter((team) => selectedTeams.includes(team.teamId));
+ }
+
+ return filtered;
+ };
+
+ const getFilteredDirectionStats = () => {
+ let filtered = directionStats;
+
+ if (selectedDirections.length > 0) {
+ filtered = filtered.filter((direction) =>
+ selectedDirections.includes(direction.direction)
+ );
+ }
+
+ // Si des équipes spécifiques sont sélectionnées, filtrer aussi les équipes dans les directions
+ if (selectedTeams.length > 0) {
+ filtered = filtered
+ .map((direction) => ({
+ ...direction,
+ teams: direction.teams.filter((team) =>
+ selectedTeams.includes(team.teamId)
+ ),
+ totalMembers: direction.teams
+ .filter((team) => selectedTeams.includes(team.teamId))
+ .reduce((sum, team) => sum + team.totalMembers, 0),
+ averageSkillLevel: direction.teams
+ .filter((team) => selectedTeams.includes(team.teamId))
+ .reduce(
+ (sum, team, _, arr) => sum + team.averageSkillLevel / arr.length,
+ 0
+ ),
+ }))
+ .filter((direction) => direction.teams.length > 0);
+ }
+
+ return filtered;
+ };
+
+ // Options pour les filtres
+ const directionOptions = Array.from(
+ new Set(teams.map((team) => team.direction))
+ ).map((direction) => ({
+ id: direction,
+ label: direction,
+ count: teams.filter((team) => team.direction === direction).length,
+ }));
+
+ const teamOptions = teams.map((team) => ({
+ id: team.id,
+ label: `${team.name} (${team.direction})`,
+ count: teamStats.find((stat) => stat.teamId === team.id)?.totalMembers || 0,
+ }));
+
+ // Fonctions pour les actions des équipes
+ const handleViewTeamDetails = (team: TeamStats) => {
+ setSelectedTeamForModal(team);
+ setIsModalOpen(true);
+ };
+
+ const handleExportTeamReport = (team: TeamStats) => {
+ const reportData = {
+ team: team.teamName,
+ direction: team.direction,
+ date: new Date().toLocaleDateString(),
+ stats: {
+ members: team.totalMembers,
+ averageLevel: team.averageSkillLevel,
+ coverage: team.skillCoverage,
+ },
+ topSkills: team.topSkills,
+ };
+
+ const dataStr = JSON.stringify(reportData, null, 2);
+ const dataUri =
+ "data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
+
+ const exportFileDefaultName = `rapport-${team.teamName.toLowerCase()}-${
+ new Date().toISOString().split("T")[0]
+ }.json`;
+
+ const linkElement = document.createElement("a");
+ linkElement.setAttribute("href", dataUri);
+ linkElement.setAttribute("download", exportFileDefaultName);
+ linkElement.click();
+ };
+
+ return (
+
+ {/* Background Effects */}
+
+
+
+
+
+ {/* Header */}
+
+
+
+
+ Administration
+
+
+
+
+ Dashboard Managérial
+
+
+
+ Vue d'ensemble des compétences par équipe et direction pour pilotage
+ stratégique
+
+
+
+ {/* Overview Cards */}
+
+
+
+
+
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? "FILTRÉES"
+ : "TOTAL"}
+
+
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? getFilteredTeamStats().length
+ : teams.length}
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? "Équipes filtrées"
+ : "Équipes"}
+
+ {(selectedDirections.length > 0 || selectedTeams.length > 0) && (
+
+ sur {teams.length} au total
+
+ )}
+
+
+
+
+
+
+
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? "FILTRÉS"
+ : "TOTAL"}
+
+
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? getFilteredTeamStats().reduce(
+ (sum, t) => sum + t.totalMembers,
+ 0
+ )
+ : teamStats.reduce((sum, t) => sum + t.totalMembers, 0)}
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? "Membres filtrés"
+ : "Membres"}
+
+ {(selectedDirections.length > 0 || selectedTeams.length > 0) && (
+
+ sur {teamStats.reduce((sum, t) => sum + t.totalMembers, 0)} au
+ total
+
+ )}
+
+
+
+
+
+
+
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? "FILTRÉES"
+ : "TOTAL"}
+
+
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? getFilteredDirectionStats().length
+ : directionStats.length}
+
+
+ {selectedDirections.length > 0 || selectedTeams.length > 0
+ ? "Directions filtrées"
+ : "Directions"}
+
+ {(selectedDirections.length > 0 || selectedTeams.length > 0) && (
+
+ sur {directionStats.length} au total
+
+ )}
+
+
+
+
+
+
+
+
+
+ RÉFÉRENTIEL
+
+
+
+
+ {skillCategories.reduce(
+ (sum, cat) => sum + (cat.skills?.length || 0),
+ 0
+ )}
+
+
Compétences suivies
+
+ {skillCategories.length} catégories
+
+
+
+
+
+ {/* Filtres */}
+
+
+
+
+
+
+ Filtres avancés
+
+
+
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ {/* Résumé des filtres actifs */}
+ {(selectedDirections.length > 0 || selectedTeams.length > 0) && (
+
+
+
+
+
+ {getFilteredTeamStats().reduce(
+ (sum, t) => sum + t.totalMembers,
+ 0
+ )}{" "}
+ membres
+
+
+
+
{
+ setSelectedDirections([]);
+ setSelectedTeams([]);
+ }}
+ className="text-xs text-slate-400 hover:text-white hover:bg-white/10"
+ >
+ Réinitialiser
+
+
+ )}
+
+
+
+ {/* Main Content Tabs */}
+
+
+
+
+ Vue par Direction
+
+
+ Vue par Équipe
+
+
+
+
+
+
+ {getFilteredDirectionStats().length > 0 ? (
+ getFilteredDirectionStats().map((direction) => (
+
+ ))
+ ) : (
+
+
+
+
+
+ Aucune direction trouvée
+
+
+ Aucune direction ne correspond aux filtres sélectionnés.
+
+
+ )}
+
+
+
+
+
+ {getFilteredTeamStats().length > 0 ? (
+ getFilteredTeamStats().map((team) => (
+
handleViewTeamDetails(team)}
+ onViewReport={() => handleExportTeamReport(team)}
+ />
+ ))
+ ) : (
+
+
+
+
+
+
+ Aucune équipe trouvée
+
+
+ Aucune équipe ne correspond aux filtres sélectionnés.
+
+
+
+ )}
+
+
+
+
+ {/* Modale de détails d'équipe */}
+
setIsModalOpen(false)}
+ team={selectedTeamForModal}
+ />
+
+
+ );
+}
diff --git a/components/admin/index.ts b/components/admin/index.ts
index f661aeb..b86a704 100644
--- a/components/admin/index.ts
+++ b/components/admin/index.ts
@@ -1,3 +1,5 @@
export { TeamStatsCard } from "./team-stats-card";
export { DirectionOverview } from "./direction-overview";
export { MultiSelectFilter } from "./multi-select-filter";
+export { AdminClientWrapper } from "./admin-client-wrapper";
+export { TeamDetailClientWrapper } from "./team-detail-client-wrapper";
diff --git a/components/admin/team-detail-client-wrapper.tsx b/components/admin/team-detail-client-wrapper.tsx
new file mode 100644
index 0000000..448fcd4
--- /dev/null
+++ b/components/admin/team-detail-client-wrapper.tsx
@@ -0,0 +1,1178 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Progress } from "@/components/ui/progress";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ Users,
+ TrendingUp,
+ Target,
+ Award,
+ BarChart3,
+ ArrowLeft,
+ Download,
+ UserCheck,
+ GraduationCap,
+ Building2,
+ Star,
+ BookOpen,
+ X,
+ User,
+ Brain,
+ Coffee,
+ Lightbulb,
+ MessageSquare,
+} from "lucide-react";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { TeamStats, TeamMember } from "@/services/admin-service";
+import { TechIcon } from "@/components/icons/tech-icon";
+
+interface TeamDetailClientWrapperProps {
+ team: TeamStats;
+ teamId: string;
+}
+
+interface SkillAnalysis {
+ skillName: string;
+ category: string;
+ experts: Array<{
+ name: string;
+ level: number;
+ canMentor: boolean;
+ }>;
+ learners: Array<{
+ name: string;
+ currentLevel: number;
+ }>;
+ averageLevel: number;
+ totalEvaluations: number;
+ expertCount: number;
+ learnerCount: number;
+ proficiencyRate: number;
+}
+
+function getSkillLevelLabel(level: number): string {
+ if (level < 0.5) return "Débutant";
+ if (level < 1.5) return "Intermé.";
+ if (level < 2.5) return "Avancé";
+ return "Expert";
+}
+
+function getSkillLevelColor(level: number): string {
+ if (level < 0.5) return "bg-red-500";
+ if (level < 1.5) return "bg-orange-500";
+ if (level < 2.5) return "bg-blue-500";
+ return "bg-green-500";
+}
+
+function getSkillLevelBadgeClasses(level: number): string {
+ if (level < 0.5) return "bg-red-500/20 border-red-500/30 text-red-300";
+ if (level < 1.5)
+ return "bg-orange-500/20 border-orange-500/30 text-orange-300";
+ if (level < 2.5) return "bg-blue-500/20 border-blue-500/30 text-blue-300";
+ return "bg-green-500/20 border-green-500/30 text-green-300";
+}
+
+export function TeamDetailClientWrapper({
+ team,
+ teamId,
+}: TeamDetailClientWrapperProps) {
+ const router = useRouter();
+ const [skillAnalysis, setSkillAnalysis] = useState([]);
+ const [selectedMember, setSelectedMember] = useState(null);
+ const [isMemberModalOpen, setIsMemberModalOpen] = useState(false);
+ const [selectedCategory, setSelectedCategory] = useState("all");
+
+ useEffect(() => {
+ // Analyser les compétences avec les vraies données
+ const skillMap = new Map();
+
+ team.members.forEach((member) => {
+ member.skills.forEach((skill) => {
+ if (!skillMap.has(skill.skillName)) {
+ skillMap.set(skill.skillName, {
+ skillName: skill.skillName,
+ category: skill.category,
+ experts: [],
+ learners: [],
+ averageLevel: 0,
+ totalEvaluations: 0,
+ });
+ }
+
+ const skillData = skillMap.get(skill.skillName);
+ skillData.totalEvaluations++;
+ skillData.averageLevel += skill.level;
+
+ if (skill.level >= 2) {
+ skillData.experts.push({
+ name: `${member.firstName} ${member.lastName}`,
+ level: skill.level,
+ canMentor: skill.canMentor,
+ });
+ }
+
+ if (skill.wantsToLearn) {
+ skillData.learners.push({
+ name: `${member.firstName} ${member.lastName}`,
+ currentLevel: skill.level,
+ });
+ }
+ });
+ });
+
+ const skills = Array.from(skillMap.values())
+ .map(
+ (skill): SkillAnalysis => ({
+ ...skill,
+ averageLevel: skill.averageLevel / skill.totalEvaluations,
+ expertCount: skill.experts.length,
+ learnerCount: skill.learners.length,
+ proficiencyRate:
+ (skill.experts.length / skill.totalEvaluations) * 100,
+ })
+ )
+ .sort((a, b) => b.averageLevel - a.averageLevel);
+
+ setSkillAnalysis(skills);
+ }, [team]);
+
+ const handleExportReport = () => {
+ const reportData = {
+ team: team.teamName,
+ direction: team.direction,
+ exportDate: new Date().toLocaleDateString(),
+ detailedStats: {
+ totalMembers: team.totalMembers,
+ averageSkillLevel: team.averageSkillLevel,
+ skillCoverage: team.skillCoverage,
+ expertiseDistribution: {
+ beginners: team.members.filter(
+ (m) =>
+ m.skills.reduce((avg, s) => avg + s.level, 0) / m.skills.length <
+ 1
+ ).length,
+ intermediate: team.members.filter((m) => {
+ const avg =
+ m.skills.reduce((avg, s) => avg + s.level, 0) / m.skills.length;
+ return avg >= 1 && avg < 2;
+ }).length,
+ advanced: team.members.filter((m) => {
+ const avg =
+ m.skills.reduce((avg, s) => avg + s.level, 0) / m.skills.length;
+ return avg >= 2 && avg < 3;
+ }).length,
+ experts: team.members.filter(
+ (m) =>
+ m.skills.reduce((avg, s) => avg + s.level, 0) / m.skills.length >=
+ 3
+ ).length,
+ },
+ },
+ members: team.members.map((member) => ({
+ name: `${member.firstName} ${member.lastName}`,
+ joinDate: member.joinDate,
+ skillCount: member.skills.length,
+ averageLevel:
+ member.skills.reduce((avg, s) => avg + s.level, 0) /
+ member.skills.length,
+ expertSkills: member.skills.filter((s) => s.level >= 2 && s.canMentor)
+ .length,
+ learningGoals: member.skills.filter((s) => s.wantsToLearn).length,
+ skills: member.skills,
+ })),
+ skillAnalysis: skillAnalysis.map((skill) => ({
+ name: skill.skillName,
+ category: skill.category,
+ averageLevel: skill.averageLevel,
+ totalEvaluations: skill.totalEvaluations,
+ expertCount: skill.expertCount,
+ learnerCount: skill.learnerCount,
+ proficiencyRate: skill.proficiencyRate,
+ experts: skill.experts,
+ learners: skill.learners,
+ })),
+ insights: {
+ topPerformingSkills: skillAnalysis.slice(0, 5).map((s) => ({
+ name: s.skillName,
+ level: s.averageLevel,
+ proficiency: s.proficiencyRate,
+ })),
+ skillsNeedingDevelopment: skillAnalysis
+ .filter((s) => s.averageLevel < 1.5)
+ .slice(0, 5)
+ .map((s) => ({
+ name: s.skillName,
+ level: s.averageLevel,
+ learners: s.learnerCount,
+ })),
+ mentorshipOpportunities: skillAnalysis
+ .filter((s) => s.experts.filter((e) => e.canMentor).length > 0)
+ .map((s) => ({
+ skill: s.skillName,
+ mentors: s.experts.filter((e) => e.canMentor).map((e) => e.name),
+ potentialMentees: s.learners.map((l) => l.name),
+ })),
+ },
+ };
+
+ const dataStr = JSON.stringify(reportData, null, 2);
+ const dataUri =
+ "data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
+
+ const exportFileDefaultName = `rapport-detaille-${team.teamName.toLowerCase()}-${
+ new Date().toISOString().split("T")[0]
+ }.json`;
+
+ const linkElement = document.createElement("a");
+ linkElement.setAttribute("href", dataUri);
+ linkElement.setAttribute("download", exportFileDefaultName);
+ linkElement.click();
+ };
+
+ const categories = Array.from(new Set(skillAnalysis.map((s) => s.category)));
+ const filteredSkills =
+ selectedCategory === "all"
+ ? skillAnalysis
+ : skillAnalysis.filter((s) => s.category === selectedCategory);
+
+ const teamInsights = {
+ averageTeamLevel:
+ team.members.reduce((sum, member) => {
+ const memberAvg =
+ member.skills.reduce((s, skill) => s + skill.level, 0) /
+ member.skills.length;
+ return sum + memberAvg;
+ }, 0) / team.members.length,
+ totalExperts: team.members.reduce(
+ (sum, member) =>
+ sum + member.skills.filter((s) => s.level >= 2 && s.canMentor).length,
+ 0
+ ),
+ totalLearners: team.members.reduce(
+ (sum, member) => sum + member.skills.filter((s) => s.wantsToLearn).length,
+ 0
+ ),
+ skillGaps: skillAnalysis.filter((s) => s.averageLevel < 1.5).length,
+ strongSkills: skillAnalysis.filter((s) => s.averageLevel >= 2.5).length,
+ };
+
+ return (
+
+ {/* Background Effects */}
+
+
+
+
+
+ {/* Header avec breadcrumb et actions */}
+
+
+
router.push("/admin")}
+ className="text-slate-400 hover:text-white hover:bg-white/10"
+ >
+
+ Retour à l'admin
+
+
|
+
+
+
+
+
+
+ {team.teamName}
+
+
{team.direction}
+
+
+
+
+
+
+ Rapport détaillé
+
+
+
+
+ {/* Métriques principales */}
+
+
+
+
+ {team.totalMembers}
+
+
personnes actives
+
+
+
+
+
+
+
+
+ Niveau équipe
+
+
+
+ {((teamInsights.averageTeamLevel / 3) * 100).toFixed(0)}%
+
+
+ {getSkillLevelLabel(teamInsights.averageTeamLevel)}
+
+
+
+
+
+
+ {teamInsights.totalExperts}
+
+
+ compétences expertes
+
+
+
+
+
+
+
+
+
+ Apprentissages
+
+
+
+ {teamInsights.totalLearners}
+
+
+ objectifs d'apprentissage
+
+
+
+
+
+
+ {teamInsights.skillGaps}
+
+
+ compétences à renforcer
+
+
+
+
+ {/* Contenu principal avec onglets */}
+
+
+
+
+ Vue d'ensemble
+
+
+ Compétences
+
+
+ Membres
+
+
+ Insights
+
+
+
+
+ {/* Onglet Vue d'ensemble */}
+
+ {/* Top Skills avec design amélioré */}
+
+
+
+ Top Compétences de l'équipe
+
+
+ {team.topSkills.slice(0, 6).map((skill, idx) => (
+
+
+
+ {skill.icon && (
+
+
+
+ )}
+
+ {skill.skillName}
+
+
+
+
+
+
+
+ {skill.averageLevel.toFixed(1)}
+
+
+ {getSkillLevelLabel(skill.averageLevel)}
+
+
+
+
+
+ ))}
+
+
+
+ {/* Distribution des niveaux */}
+
+
+
+
+ Répartition des niveaux
+
+
+ {[
+ {
+ label: "Expert",
+ count: team.members.filter(
+ (m) =>
+ m.skills.reduce((avg, s) => avg + s.level, 0) /
+ m.skills.length >=
+ 2.5
+ ).length,
+ color: "bg-green-500",
+ },
+ {
+ label: "Avancé",
+ count: team.members.filter((m) => {
+ const avg =
+ m.skills.reduce((avg, s) => avg + s.level, 0) /
+ m.skills.length;
+ return avg >= 2 && avg < 2.5;
+ }).length,
+ color: "bg-blue-500",
+ },
+ {
+ label: "Intermédiaire",
+ count: team.members.filter((m) => {
+ const avg =
+ m.skills.reduce((avg, s) => avg + s.level, 0) /
+ m.skills.length;
+ return avg >= 1 && avg < 2;
+ }).length,
+ color: "bg-orange-500",
+ },
+ {
+ label: "Débutant",
+ count: team.members.filter(
+ (m) =>
+ m.skills.reduce((avg, s) => avg + s.level, 0) /
+ m.skills.length <
+ 1
+ ).length,
+ color: "bg-red-500",
+ },
+ ].map((level, idx) => (
+
+
+
+
+ {level.count}
+
+
+
+ {((level.count / team.totalMembers) * 100).toFixed(0)}
+ %
+
+
+
+ ))}
+
+
+
+
+
+
+ Métriques clés
+
+
+
+
+ Couverture des compétences
+
+
+ {team.skillCoverage.toFixed(1)}%
+
+
+
+ Compétences fortes
+
+ {teamInsights.strongSkills}
+
+
+
+
+ Objectifs d'apprentissage
+
+
+ {teamInsights.totalLearners}
+
+
+
+ Mentors disponibles
+
+ {skillAnalysis.reduce(
+ (sum, skill) =>
+ sum + skill.experts.filter((e) => e.canMentor).length,
+ 0
+ )}
+
+
+
+
+
+
+
+ {/* Onglet Compétences */}
+
+ {/* Filtres par catégorie */}
+
+
+ setSelectedCategory("all")}
+ className={
+ selectedCategory === "all"
+ ? "bg-blue-500 text-white"
+ : "text-slate-400 hover:text-white hover:bg-white/10"
+ }
+ >
+ Toutes ({skillAnalysis.length})
+
+ {categories.map((category) => (
+ setSelectedCategory(category)}
+ className={
+ selectedCategory === category
+ ? "bg-blue-500 text-white"
+ : "text-slate-400 hover:text-white hover:bg-white/10"
+ }
+ >
+ {category} (
+ {
+ skillAnalysis.filter((s) => s.category === category)
+ .length
+ }
+ )
+
+ ))}
+
+
+
+ {/* Liste des compétences détaillée */}
+
+ {filteredSkills.map((skill, idx) => (
+
+
+
+
+ {skill.skillName}
+
+
{skill.category}
+
+
+
+ {getSkillLevelLabel(skill.averageLevel)}
+
+
+
+
+
+
+
+ Niveau moyen:
+
+
+ {skill.averageLevel.toFixed(1)}/3
+
+
+
+
+
+
+
+
+ {skill.totalEvaluations}
+
+
Évaluations
+
+
+
+ {skill.expertCount}
+
+
Experts
+
+
+
+ {skill.learnerCount}
+
+
Apprenants
+
+
+
+ {skill.experts.filter((e) => e.canMentor).length > 0 && (
+
+
+ Mentors disponibles:
+
+
+ {skill.experts
+ .filter((e) => e.canMentor)
+ .slice(0, 2)
+ .map((expert, i) => (
+
+ {expert.name}
+
+ ))}
+ {skill.experts.filter((e) => e.canMentor).length >
+ 2 && (
+
+ +
+ {skill.experts.filter((e) => e.canMentor).length -
+ 2}
+
+ )}
+
+
+ )}
+
+
+ ))}
+
+
+
+ {/* Onglet Membres */}
+
+
+ {team.members.map((member) => {
+ const memberAvg =
+ member.skills.reduce((sum, skill) => sum + skill.level, 0) /
+ member.skills.length;
+ const expertSkills = member.skills.filter(
+ (s) => s.level >= 2 && s.canMentor
+ ).length;
+ const learningGoals = member.skills.filter(
+ (s) => s.wantsToLearn
+ ).length;
+
+ return (
+
{
+ setSelectedMember(member);
+ setIsMemberModalOpen(true);
+ }}
+ >
+
+
+
+
+
+
+
+ {member.firstName} {member.lastName}
+
+
+ {new Date(member.joinDate).toLocaleDateString()}
+
+
+
+
+
+ {getSkillLevelLabel(memberAvg)}
+
+
+
+
+
+
+ Niveau moyen:
+
+ {memberAvg.toFixed(1)}/3
+
+
+
+
+
+
+
+
+ {member.skills.length}
+
+
Compétences
+
+
+
+ {expertSkills}
+
+
Expertises
+
+
+
+ {learningGoals}
+
+
Objectifs
+
+
+
+ {expertSkills > 0 && (
+
+
+
+ Peut mentorer {expertSkills} compétence
+ {expertSkills > 1 ? "s" : ""}
+
+
+ )}
+
+ {learningGoals > 0 && (
+
+
+
+ Souhaite apprendre {learningGoals} compétence
+ {learningGoals > 1 ? "s" : ""}
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+ {/* Onglet Insights */}
+
+
+ {/* Compétences à développer */}
+
+
+
+ Compétences à développer
+
+
+ {skillAnalysis
+ .filter((s) => s.averageLevel < 1.5)
+ .slice(0, 5)
+ .map((skill, idx) => (
+
+
+
+ {skill.skillName}
+
+
+ {skill.category}
+
+
+
+
+ {skill.averageLevel.toFixed(1)}
+
+
+ {skill.learnerCount} intéressés
+
+
+
+ ))}
+
+
+
+ {/* Opportunités de mentorat */}
+
+
+
+ Opportunités de mentorat
+
+
+ {skillAnalysis
+ .filter(
+ (s) =>
+ s.experts.filter((e) => e.canMentor).length > 0 &&
+ s.learnerCount > 0
+ )
+ .slice(0, 5)
+ .map((skill, idx) => (
+
+
+
+ {skill.skillName}
+
+
+ {skill.category}
+
+
+
+
+ {skill.experts.filter((e) => e.canMentor).length}{" "}
+ mentor
+ {skill.experts.filter((e) => e.canMentor).length > 1
+ ? "s"
+ : ""}
+
+
+ {skill.learnerCount} apprenant
+ {skill.learnerCount > 1 ? "s" : ""}
+
+
+
+ ))}
+
+
+
+
+ {/* Recommandations */}
+
+
+
+ Recommandations pour l'équipe
+
+
+
+
+ 🎯 Formations prioritaires
+
+
+ Organiser des formations sur{" "}
+ {skillAnalysis
+ .filter((s) => s.averageLevel < 1.5)
+ .slice(0, 2)
+ .map((s) => s.skillName)
+ .join(" et ")}
+ pour combler les lacunes identifiées.
+
+
+
+
+
+ 🤝 Programme de mentorat
+
+
+ Mettre en place un système de mentorat avec{" "}
+ {skillAnalysis.reduce(
+ (sum, skill) =>
+ sum + skill.experts.filter((e) => e.canMentor).length,
+ 0
+ )}{" "}
+ mentors disponibles.
+
+
+
+
+
+ 📈 Capitaliser sur les forces
+
+
+ Exploiter l'expertise en{" "}
+ {skillAnalysis
+ .filter((s) => s.averageLevel >= 2.5)
+ .slice(0, 2)
+ .map((s) => s.skillName)
+ .join(" et ")}
+ pour des projets ambitieux.
+
+
+
+
+
+ 🔄 Plan de développement
+
+
+ Créer des parcours de montée en compétences personnalisés
+ pour {teamInsights.totalLearners} objectifs d'apprentissage.
+
+
+
+
+
+
+
+ {/* Modal détail membre */}
+
+
+
+
+ {selectedMember && (
+
+
+
+
+
+
+ {selectedMember.firstName} {selectedMember.lastName}
+
+
+ Membre depuis le{" "}
+ {new Date(selectedMember.joinDate).toLocaleDateString()}
+
+
+
+ )}
+ setIsMemberModalOpen(false)}
+ className="text-slate-400 hover:text-white"
+ >
+
+
+
+
+
+ {selectedMember && (
+
+ {/* Stats du membre */}
+
+
+
+ {selectedMember.skills.length}
+
+
+ Compétences évaluées
+
+
+
+
+ {
+ selectedMember.skills.filter(
+ (s) => s.level >= 2 && s.canMentor
+ ).length
+ }
+
+
Peut mentorer
+
+
+
+ {
+ selectedMember.skills.filter((s) => s.wantsToLearn)
+ .length
+ }
+
+
+ Souhaite apprendre
+
+
+
+
+ {/* Compétences détaillées */}
+
+
+ Portfolio de compétences
+
+
+ {selectedMember.skills
+ .sort((a, b) => b.level - a.level)
+ .map((skill, idx) => (
+
+
+
+
+
+ {skill.skillName}
+
+
+ {skill.category}
+
+
+
+
+ {getSkillLevelLabel(skill.level)}
+
+
+
+
+
+
+
+ {skill.canMentor && (
+
+
+ Mentor
+
+ )}
+ {skill.wantsToLearn && (
+
+
+ Apprenant
+
+ )}
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/components/admin/team-detail-modal.tsx b/components/admin/team-detail-modal.tsx
index d7e0ca8..bf03a48 100644
--- a/components/admin/team-detail-modal.tsx
+++ b/components/admin/team-detail-modal.tsx
@@ -11,27 +11,9 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
-import {
- Users,
- ExternalLink,
- Download,
- Eye,
-} from "lucide-react";
+import { Users, ExternalLink, Download, Eye } from "lucide-react";
-interface TeamMember {
- id: string;
- firstName: string;
- lastName: string;
- skills: Array<{
- skillId: string;
- skillName: string;
- category: string;
- level: number;
- canMentor: boolean;
- wantsToLearn: boolean;
- }>;
- joinDate: string;
-}
+import { TeamMember } from "@/services/admin-service";
interface TeamDetailModalProps {
isOpen: boolean;
@@ -42,7 +24,11 @@ interface TeamDetailModalProps {
direction: string;
totalMembers: number;
averageSkillLevel: number;
- topSkills: Array<{ skillName: string; averageLevel: number }>;
+ topSkills: Array<{
+ skillName: string;
+ averageLevel: number;
+ icon?: string;
+ }>;
skillCoverage: number;
members: TeamMember[];
} | null;
@@ -68,7 +54,7 @@ export function TeamDetailModal({
team,
}: TeamDetailModalProps) {
const router = useRouter();
-
+
if (!team) return null;
const handleViewFullDetails = () => {
@@ -120,7 +106,8 @@ export function TeamDetailModal({
- Aperçu rapide de l'équipe. Cliquez sur "Voir tous les détails" pour une vue complète.
+ Aperçu rapide de l'équipe. Cliquez sur "Voir tous les détails" pour
+ une vue complète.
@@ -155,14 +142,19 @@ export function TeamDetailModal({
Top 3 Compétences
{team.topSkills.slice(0, 3).map((skill, idx) => (
-
+
{skill.skillName}
{((skill.averageLevel / 3) * 100).toFixed(0)}%
diff --git a/services/admin-service.ts b/services/admin-service.ts
new file mode 100644
index 0000000..4f65de7
--- /dev/null
+++ b/services/admin-service.ts
@@ -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
{
+ 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();
+
+ 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");
+ }
+ }
+}
diff --git a/services/index.ts b/services/index.ts
index 0efe391..eb633ee 100644
--- a/services/index.ts
+++ b/services/index.ts
@@ -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";