266 lines
8.0 KiB
TypeScript
266 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { TeamStats, TeamMember } from "@/lib/admin-types";
|
|
import { TeamDetailHeader } from "./team-detail-header";
|
|
import { TeamMetricsCards } from "./team-metrics-cards";
|
|
import { TeamDetailTabs } from "./team-detail-tabs";
|
|
import { TeamMemberModal } from "@/components/admin";
|
|
import {
|
|
COVERAGE_OBJECTIVES,
|
|
isCoverageBelowObjective,
|
|
} from "@/lib/evaluation-utils";
|
|
|
|
interface TeamDetailClientWrapperProps {
|
|
team: TeamStats;
|
|
teamId: string;
|
|
}
|
|
|
|
interface SkillAnalysis {
|
|
skillName: string;
|
|
category: string;
|
|
importance: "incontournable" | "majeure" | "standard";
|
|
experts: Array<{
|
|
name: string;
|
|
level: number;
|
|
canMentor: boolean;
|
|
}>;
|
|
learners: Array<{
|
|
name: string;
|
|
currentLevel: number;
|
|
}>;
|
|
averageLevel: number;
|
|
totalEvaluations: number;
|
|
expertCount: number;
|
|
learnerCount: number;
|
|
proficiencyRate: number;
|
|
coverage: number;
|
|
}
|
|
|
|
interface TeamInsights {
|
|
averageTeamLevel: number;
|
|
totalExperts: number;
|
|
totalLearners: number;
|
|
skillGaps: {
|
|
incontournable: number;
|
|
majeure: number;
|
|
standard: number;
|
|
};
|
|
strongSkills: {
|
|
incontournable: number;
|
|
majeure: number;
|
|
standard: number;
|
|
};
|
|
criticalSkillsCoverage: {
|
|
incontournable: number;
|
|
majeure: number;
|
|
};
|
|
}
|
|
|
|
export function TeamDetailClientWrapper({
|
|
team,
|
|
teamId,
|
|
}: TeamDetailClientWrapperProps) {
|
|
const [skillAnalysis, setSkillAnalysis] = useState<SkillAnalysis[]>([]);
|
|
const [selectedMember, setSelectedMember] = useState<TeamMember | null>(null);
|
|
const [isMemberModalOpen, setIsMemberModalOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Analyser les compétences avec les vraies données
|
|
const skillMap = new Map<string, any>();
|
|
|
|
// Créer un Map pour stocker les infos d'importance des skills
|
|
const skillImportanceMap = new Map<string, string>();
|
|
team.topSkills.forEach((skill) => {
|
|
skillImportanceMap.set(skill.skillName, skill.importance);
|
|
});
|
|
|
|
team.members.forEach((member) => {
|
|
member.skills.forEach((skill) => {
|
|
if (!skillMap.has(skill.skillName)) {
|
|
skillMap.set(skill.skillName, {
|
|
skillName: skill.skillName,
|
|
category: skill.category,
|
|
importance: skillImportanceMap.get(skill.skillName) || "standard",
|
|
experts: [],
|
|
learners: [],
|
|
averageLevel: 0,
|
|
totalEvaluations: 0,
|
|
coverage: 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,
|
|
coverage: (skill.experts.length / team.totalMembers) * 100,
|
|
})
|
|
)
|
|
.sort((a, b) => {
|
|
// D'abord par importance
|
|
const importanceOrder = { incontournable: 2, majeure: 1, standard: 0 };
|
|
const importanceDiff =
|
|
importanceOrder[b.importance] - importanceOrder[a.importance];
|
|
if (importanceDiff !== 0) return importanceDiff;
|
|
|
|
// Ensuite par niveau moyen
|
|
return 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,
|
|
},
|
|
members: team.members.map((member) => ({
|
|
name: `${member.firstName} ${member.lastName}`,
|
|
joinDate: member.joinDate,
|
|
skillCount: member.skills.length,
|
|
skills: member.skills,
|
|
})),
|
|
skillAnalysis,
|
|
};
|
|
|
|
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 teamInsights: 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: {
|
|
incontournable: skillAnalysis.filter(
|
|
(s) =>
|
|
s.importance === "incontournable" &&
|
|
isCoverageBelowObjective(s.coverage, s.importance)
|
|
).length,
|
|
majeure: skillAnalysis.filter(
|
|
(s) =>
|
|
s.importance === "majeure" &&
|
|
isCoverageBelowObjective(s.coverage, s.importance)
|
|
).length,
|
|
standard: skillAnalysis.filter(
|
|
(s) => s.importance === "standard" && s.averageLevel < 1.5
|
|
).length,
|
|
},
|
|
strongSkills: {
|
|
incontournable: skillAnalysis.filter(
|
|
(s) =>
|
|
s.importance === "incontournable" &&
|
|
!isCoverageBelowObjective(s.coverage, s.importance)
|
|
).length,
|
|
majeure: skillAnalysis.filter(
|
|
(s) =>
|
|
s.importance === "majeure" &&
|
|
!isCoverageBelowObjective(s.coverage, s.importance)
|
|
).length,
|
|
standard: skillAnalysis.filter(
|
|
(s) => s.importance === "standard" && s.averageLevel >= 2.5
|
|
).length,
|
|
},
|
|
criticalSkillsCoverage: team.criticalSkillsCoverage,
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
|
|
{/* Background Effects */}
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
|
|
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
|
|
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent" />
|
|
|
|
<div className="relative z-10 container mx-auto px-6 py-8 max-w-7xl space-y-8">
|
|
{/* Header */}
|
|
<TeamDetailHeader
|
|
teamName={team.teamName}
|
|
direction={team.direction}
|
|
onExport={handleExportReport}
|
|
/>
|
|
|
|
{/* Métriques principales */}
|
|
<TeamMetricsCards
|
|
totalMembers={team.totalMembers}
|
|
teamInsights={teamInsights}
|
|
skillCoverage={team.skillCoverage}
|
|
/>
|
|
|
|
{/* Contenu principal avec onglets */}
|
|
<TeamDetailTabs
|
|
team={team}
|
|
skillAnalysis={skillAnalysis}
|
|
teamInsights={teamInsights}
|
|
onMemberClick={(member) => {
|
|
setSelectedMember(member);
|
|
setIsMemberModalOpen(true);
|
|
}}
|
|
/>
|
|
|
|
{/* Modal détail membre */}
|
|
<TeamMemberModal
|
|
isOpen={isMemberModalOpen}
|
|
onClose={() => setIsMemberModalOpen(false)}
|
|
member={selectedMember}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|