diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 4ff80f6..09ac96e 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,698 +1,39 @@ -"use client"; +import { redirect } from "next/navigation"; +import { isUserAuthenticated } from "@/lib/server-auth"; +import { AdminService } from "@/services/admin-service"; +import { AdminClientWrapper } from "@/components/admin"; -import { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Badge } from "@/components/ui/badge"; -import { Progress } from "@/components/ui/progress"; -import { Button } from "@/components/ui/button"; -import { - Users, - TrendingUp, - BarChart3, - Target, - Building2, - UserCheck, - Filter, -} from "lucide-react"; -import { loadTeams, loadSkillCategories } from "@/lib/data-loader"; -import { Team, SkillCategory, UserEvaluation } from "@/lib/types"; -import { - TeamStatsCard, - DirectionOverview, - MultiSelectFilter, -} from "@/components/admin"; -import { TeamDetailModal } from "@/components/admin/team-detail-modal"; +export default async function AdminPage() { + // Vérifier l'authentification + const isAuthenticated = await isUserAuthenticated(); -interface TeamMember { - id: string; - firstName: string; - lastName: string; - skills: Array<{ - skillId: string; - skillName: string; - category: string; - level: number; - canMentor: boolean; - wantsToLearn: boolean; - }>; - joinDate: string; -} + // Si pas de cookie d'authentification, rediriger vers login + if (!isAuthenticated) { + redirect("/login"); + } -interface TeamStats { - teamId: string; - teamName: string; - direction: string; - totalMembers: number; - averageSkillLevel: number; - topSkills: Array<{ skillName: string; averageLevel: number }>; - skillCoverage: number; // Percentage of skills evaluated - members: TeamMember[]; -} + // Charger les données côté serveur + try { + const adminData = await AdminService.getAdminData(); -interface DirectionStats { - direction: string; - teams: TeamStats[]; - totalMembers: number; - averageSkillLevel: number; - topCategories: Array<{ category: string; averageLevel: number }>; -} - -// Helper function to generate realistic team members -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)]; - - // Each member evaluates 8-15 random skills - 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), // 0-3 - canMentor: Math.random() > 0.7, // 30% chance - wantsToLearn: Math.random() > 0.6, // 40% chance - })); - - 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], // Random date in last 3 years - }; - }); -} - -// Helper function to get random skills from categories -function getRandomSkills(skillCategories: SkillCategory[], count: number = 3) { - const allSkills: Array<{ - skillName: string; - category: string; - icon: string; - }> = []; - - skillCategories.forEach((category) => { - category.skills.forEach((skill) => { - allSkills.push({ - skillName: skill.name, - category: category.category, - icon: skill.icon || "", - }); - }); - }); - - // Shuffle and pick random skills - const shuffled = allSkills.sort(() => Math.random() - 0.5); - const randomSkills = shuffled.slice(0, count).map((skill) => ({ - skillName: skill.skillName, - averageLevel: Math.random() * 3, - icon: skill.icon, - })); - - // Sort by average level (descending - highest first) - return randomSkills.sort((a, b) => b.averageLevel - a.averageLevel); -} - -// Mock data generator - à remplacer par de vraies données -function generateMockTeamStats( - teams: Team[], - skillCategories: SkillCategory[] -): TeamStats[] { - return teams.map((team) => { - const memberCount = Math.floor(Math.random() * 10) + 3; // 3-12 members - const members = generateTeamMembers( - team.name, - memberCount, - skillCategories + return ( + ); - - return { - teamId: team.id, - teamName: team.name, - direction: team.direction, - totalMembers: memberCount, - averageSkillLevel: Math.random() * 3, // 0-3 - topSkills: getRandomSkills(skillCategories, 3), - skillCoverage: Math.random() * 100, // 0-100% - members: members, - }; - }); -} - -function 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.reduce((sum, t) => sum + t.averageSkillLevel, 0) / - directionTeams.length; - - return { - direction, - teams: directionTeams, - totalMembers, - averageSkillLevel, - topCategories: [ - { category: "Frontend", averageLevel: Math.random() * 3 }, - { category: "Backend", averageLevel: Math.random() * 3 }, - { category: "DevOps", averageLevel: Math.random() * 3 }, - ], - }; - }); -} - -export default function AdminPage() { - const [teams, setTeams] = useState([]); - const [skillCategories, setSkillCategories] = useState([]); - const [teamStats, setTeamStats] = useState([]); - const [directionStats, setDirectionStats] = useState([]); - const [loading, setLoading] = useState(true); - - // Filtres - const [selectedDirections, setSelectedDirections] = useState([]); - const [selectedTeams, setSelectedTeams] = useState([]); - - // Modale de détails - const [selectedTeamForModal, setSelectedTeamForModal] = - useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - - useEffect(() => { - async function loadData() { - try { - const [teamsData, categoriesData] = await Promise.all([ - loadTeams(), - loadSkillCategories(), - ]); - - setTeams(teamsData); - setSkillCategories(categoriesData); - - // Generate mock stats - const mockTeamStats = generateMockTeamStats(teamsData, categoriesData); - const mockDirectionStats = generateDirectionStats(mockTeamStats); - - setTeamStats(mockTeamStats); - setDirectionStats(mockDirectionStats); - } catch (error) { - console.error("Failed to load admin data:", error); - } finally { - setLoading(false); - } - } - - loadData(); - }, []); - - // 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(); - }; - - if (loading) { + } catch (error) { + console.error("Failed to load admin data:", error); return (
-
- 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 - -
-
- -
- )} -
-
- - {/* 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 ( -
-
-
-
-
-
Chargement...
-
+ + ); + } catch (error) { + console.error("Failed to load team data:", error); + return ( +
+
+ Erreur lors du chargement des détails de l'équipe
); } - - if (!team) { - return ( -
-
-
-
-
Équipe non trouvée
-
-
- ); - } - - return ( -
-
-
- -
- {/* Header */} -
-
-
- -
-
- -
-
-

- Équipe {team.teamName} -

-

- {team.direction} • {team.totalMembers} membres -

-
-
-
- -
-
- - {/* 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 + +
+
+ +
+ )} +
+
+ + {/* 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 */} +
+
+ +
|
+
+
+ +
+
+

+ {team.teamName} +

+

{team.direction}

+
+
+
+
+ +
+
+ + {/* Métriques principales */} +
+
+
+
+ +
+ + Membres + +
+
+ {team.totalMembers} +
+
personnes actives
+
+ +
+
+
+ +
+ + Niveau équipe + +
+
+ {((teamInsights.averageTeamLevel / 3) * 100).toFixed(0)}% +
+
+ {getSkillLevelLabel(teamInsights.averageTeamLevel)} +
+
+ +
+
+
+ +
+ + Expertises + +
+
+ {teamInsights.totalExperts} +
+
+ compétences expertes +
+
+ +
+
+
+ +
+ + Apprentissages + +
+
+ {teamInsights.totalLearners} +
+
+ objectifs d'apprentissage +
+
+ +
+
+
+ +
+ + Lacunes + +
+
+ {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.label} + +
+
+ + {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 */} +
+
+ + {categories.map((category) => ( + + ))} +
+
+ + {/* 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()} +

+
+
+ )} + +
+
+ + {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";