diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..4ff80f6 --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,698 @@ +"use client"; + +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"; + +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 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[]; +} + +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 { + 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) { + return ( +
+
+
+ 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 new file mode 100644 index 0000000..c8fd1ce --- /dev/null +++ b/app/admin/team/[teamId]/page.tsx @@ -0,0 +1,950 @@ +"use client"; + +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 TeamStats { + teamId: string; + teamName: string; + direction: string; + totalMembers: number; + averageSkillLevel: number; + topSkills: Array<{ skillName: string; averageLevel: number }>; + skillCoverage: number; + members: TeamMember[]; +} + +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"; +} + +// 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); + } + } + + 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...
+
+
+
+ ); + } + + 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/direction-overview.tsx b/components/admin/direction-overview.tsx new file mode 100644 index 0000000..08c665f --- /dev/null +++ b/components/admin/direction-overview.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { Building2, Users, TrendingUp, BarChart3, Target } from "lucide-react"; +import { + TeamStatsCard, + getSkillLevelLabel, + getSkillLevelColor, +} from "./team-stats-card"; + +interface DirectionOverviewProps { + direction: string; + teams: Array<{ + teamId: string; + teamName: string; + direction: string; + totalMembers: number; + averageSkillLevel: number; + topSkills: Array<{ skillName: string; averageLevel: number }>; + skillCoverage: number; + }>; + totalMembers: number; + averageSkillLevel: number; + topCategories: Array<{ category: string; averageLevel: number }>; + onViewTeamDetails?: (team: any) => void; + onExportTeamReport?: (team: any) => void; +} + +export function DirectionOverview({ + direction, + teams, + totalMembers, + averageSkillLevel, + topCategories, + onViewTeamDetails = () => {}, + onExportTeamReport = () => {}, +}: DirectionOverviewProps) { + return ( +
+
+
+
+
+ +
+

+ Direction {direction} +

+
+
+
+ + + {totalMembers} + +
+
+ + Niveau: {getSkillLevelLabel(averageSkillLevel)} + +
+
+
+
+ +
+ {/* Direction Metrics */} +
+ {/* Top Categories */} +
+
+ +

+ Top Catégories +

+
+
+ {topCategories.slice(0, 4).map((cat, idx) => ( +
+ {cat.category} +
+
+ + {cat.averageLevel.toFixed(1)} + +
+
+ ))} +
+
+ + {/* Overall Progress */} +
+
+ +

+ Progression Globale +

+
+
+
+
+ + Maîtrise globale: + + + {((averageSkillLevel / 3) * 100).toFixed(0)}% + +
+
+
+
+
+
+ Basé sur {teams.length} équipes +
+
+
+ + {/* Team Distribution */} +
+
+ +

+ Répartition +

+
+
+
+
+ {teams.length} +
+
équipes actives
+
+
+
+ Membres par équipe: + + {(totalMembers / teams.length).toFixed(1)} + +
+
+ Équipes performantes: + + {teams.filter((t) => t.averageSkillLevel > 2).length} + +
+
+
+
+
+ + {/* Teams Grid */} +
+
+
+ +
+

+ Équipes de la direction +

+
+
+ {teams.map((team) => ( + onViewTeamDetails(team)} + onViewReport={() => onExportTeamReport(team)} + /> + ))} +
+
+
+
+ ); +} diff --git a/components/admin/index.ts b/components/admin/index.ts new file mode 100644 index 0000000..f661aeb --- /dev/null +++ b/components/admin/index.ts @@ -0,0 +1,3 @@ +export { TeamStatsCard } from "./team-stats-card"; +export { DirectionOverview } from "./direction-overview"; +export { MultiSelectFilter } from "./multi-select-filter"; diff --git a/components/admin/multi-select-filter.tsx b/components/admin/multi-select-filter.tsx new file mode 100644 index 0000000..4b425b1 --- /dev/null +++ b/components/admin/multi-select-filter.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Filter, X, ChevronDown } from "lucide-react"; + +interface FilterOption { + id: string; + label: string; + count?: number; +} + +interface MultiSelectFilterProps { + title: string; + options: FilterOption[]; + selectedValues: string[]; + onChange: (selectedValues: string[]) => void; + placeholder?: string; + icon?: React.ReactNode; +} + +export function MultiSelectFilter({ + title, + options, + selectedValues, + onChange, + placeholder = "Sélectionner...", + icon = , +}: MultiSelectFilterProps) { + const [isOpen, setIsOpen] = useState(false); + + const handleToggle = (value: string) => { + const newSelected = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + onChange(newSelected); + }; + + const handleSelectAll = () => { + if (selectedValues.length === options.length) { + onChange([]); + } else { + onChange(options.map((opt) => opt.id)); + } + }; + + const clearSelection = () => { + onChange([]); + }; + + const getDisplayText = () => { + if (selectedValues.length === 0) return placeholder; + if (selectedValues.length === 1) { + const option = options.find((opt) => opt.id === selectedValues[0]); + return option?.label || selectedValues[0]; + } + return `${selectedValues.length} sélectionné(s)`; + }; + + return ( +
+ + + + + + +
+
+ {/* Header avec actions */} +
+ {title} +
+ + {selectedValues.length > 0 && ( + + )} +
+
+ + {/* Options */} +
+ {options.map((option) => ( +
handleToggle(option.id)} + > + {}} // Géré par le onClick du div parent + /> + +
+ ))} +
+ + {/* Résumé de la sélection */} + {selectedValues.length > 0 && ( +
+
+ {selectedValues.slice(0, 3).map((value) => { + const option = options.find((opt) => opt.id === value); + return ( +
+ + {option?.label || value} + +
+ ); + })} + {selectedValues.length > 3 && ( +
+ + +{selectedValues.length - 3} + +
+ )} +
+
+ )} +
+
+
+
+
+ ); +} diff --git a/components/admin/team-detail-modal.tsx b/components/admin/team-detail-modal.tsx new file mode 100644 index 0000000..d7e0ca8 --- /dev/null +++ b/components/admin/team-detail-modal.tsx @@ -0,0 +1,195 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +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; +} + +interface TeamDetailModalProps { + isOpen: boolean; + onClose: () => void; + team: { + teamId: string; + teamName: string; + direction: string; + totalMembers: number; + averageSkillLevel: number; + topSkills: Array<{ skillName: string; averageLevel: number }>; + skillCoverage: number; + members: TeamMember[]; + } | null; +} + +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"; +} + +export function TeamDetailModal({ + isOpen, + onClose, + team, +}: TeamDetailModalProps) { + const router = useRouter(); + + if (!team) return null; + + const handleViewFullDetails = () => { + router.push(`/admin/team/${team.teamId}`); + onClose(); + }; + + const handleExportReport = () => { + // Rapport simple depuis la modale + 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, + }; + + const dataStr = JSON.stringify(reportData, null, 2); + const dataUri = + "data:application/json;charset=utf-8," + encodeURIComponent(dataStr); + + const exportFileDefaultName = `apercu-${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 ( + + + + +
+ +
+
+

Équipe {team.teamName}

+

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

+
+
+ + Aperçu rapide de l'équipe. Cliquez sur "Voir tous les détails" pour une vue complète. + +
+ +
+ {/* Stats générales */} +
+
+ +
+ {team.totalMembers} +
+
Membres
+
+
+ +
+ {((team.averageSkillLevel / 3) * 100).toFixed(0)}% +
+
Niveau moyen
+
+
+ +
+ {team.skillCoverage.toFixed(0)}% +
+
Couverture
+
+
+ + {/* Top compétences résumé */} +
+

Top 3 Compétences

+
+ {team.topSkills.slice(0, 3).map((skill, idx) => ( +
+ {skill.skillName} +
+ + {((skill.averageLevel / 3) * 100).toFixed(0)}% + +
+
+
+ ))} +
+
+ + {/* Actions */} +
+ + +
+
+ +
+ ); +} diff --git a/components/admin/team-stats-card.tsx b/components/admin/team-stats-card.tsx new file mode 100644 index 0000000..b4963a4 --- /dev/null +++ b/components/admin/team-stats-card.tsx @@ -0,0 +1,259 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Users, + TrendingUp, + BarChart3, + ExternalLink, + Target, + Award, + Star, +} from "lucide-react"; +import { TechIcon } from "@/components/icons/tech-icon"; + +interface TeamStatsCardProps { + teamId: string; + teamName: string; + direction: string; + totalMembers: number; + averageSkillLevel: number; + topSkills: Array<{ + skillName: string; + averageLevel: number; + color?: string; + icon?: string; + }>; + skillCoverage: number; + onViewDetails?: () => void; + onViewReport?: () => void; +} + +export 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"; +} + +export 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"; +} + +export 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 getProgressColor(percentage: number): string { + if (percentage < 30) return "bg-red-500"; + if (percentage < 60) return "bg-orange-500"; + if (percentage < 80) return "bg-blue-500"; + return "bg-green-500"; +} + +export function TeamStatsCard({ + teamId, + teamName, + direction, + totalMembers, + averageSkillLevel, + topSkills, + skillCoverage, + onViewDetails, + onViewReport, +}: TeamStatsCardProps) { + return ( +
+
+
+
+ +
+

{teamName}

+
+
+
+ + {direction} + +
+
+
+ + + {totalMembers} + +
+
+
+
+ +
+ {/* Stats Overview */} +
+
+
+ + + Niveau moyen + +
+
+
+ + {((averageSkillLevel / 3) * 100).toFixed(0)}% + +
+ + {getSkillLevelLabel(averageSkillLevel)} + +
+
+
+
+
+
+
+ +
+
+ + + Couverture + +
+
+
+ + {skillCoverage.toFixed(0)}% + + des compétences +
+
+
+
+
+
+
+ + {/* Top Skills */} +
+
+
+ +
+ + Top Compétences + +
+
+ {topSkills.slice(0, 3).map((skill, idx) => ( +
+ {/* Icône de la technologie */} +
+ {skill.icon ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ + {/* Nom et niveau */} +
+
+ + {skill.skillName} + +
+ + {skill.averageLevel.toFixed(1)} + +
+
+
+ + {/* Mini barre de progression */} +
+
+
+
+
+ ))} +
+
+ + {/* Action Buttons */} +
+ + + +
+
+
+ ); +} diff --git a/components/layout/navigation.tsx b/components/layout/navigation.tsx index 596fb51..311ca5b 100644 --- a/components/layout/navigation.tsx +++ b/components/layout/navigation.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { Button } from "@/components/ui/button"; import { ThemeToggle } from "@/components/layout/theme-toggle"; -import { BarChart3, User, Settings } from "lucide-react"; +import { BarChart3, User, Settings, Building2 } from "lucide-react"; interface NavigationProps { userInfo?: { @@ -28,6 +28,11 @@ export function Navigation({ userInfo }: NavigationProps = {}) { label: "Évaluation", icon: User, }, + { + href: "/admin", + label: "Administration", + icon: Building2, + }, ]; return (