feat: review admin overview and popup details fwith importance
This commit is contained in:
@@ -15,8 +15,17 @@ interface DirectionOverviewProps {
|
|||||||
direction: string;
|
direction: string;
|
||||||
totalMembers: number;
|
totalMembers: number;
|
||||||
averageSkillLevel: number;
|
averageSkillLevel: number;
|
||||||
topSkills: Array<{ skillName: string; averageLevel: number }>;
|
topSkills: Array<{
|
||||||
|
skillName: string;
|
||||||
|
averageLevel: number;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
|
coverage: number;
|
||||||
|
}>;
|
||||||
skillCoverage: number;
|
skillCoverage: number;
|
||||||
|
criticalSkillsCoverage: {
|
||||||
|
incontournable: number;
|
||||||
|
majeure: number;
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
totalMembers: number;
|
totalMembers: number;
|
||||||
averageSkillLevel: number;
|
averageSkillLevel: number;
|
||||||
@@ -81,6 +90,18 @@ export function DirectionOverview({
|
|||||||
}: DirectionOverviewProps) {
|
}: DirectionOverviewProps) {
|
||||||
const colors = getDirectionColors(direction);
|
const colors = getDirectionColors(direction);
|
||||||
|
|
||||||
|
// Calculer la moyenne des couvertures des compétences critiques
|
||||||
|
const averageCriticalCoverage = teams.reduce(
|
||||||
|
(acc, team) => {
|
||||||
|
acc.incontournable += team.criticalSkillsCoverage.incontournable;
|
||||||
|
acc.majeure += team.criticalSkillsCoverage.majeure;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ incontournable: 0, majeure: 0 }
|
||||||
|
);
|
||||||
|
averageCriticalCoverage.incontournable /= teams.length || 1;
|
||||||
|
averageCriticalCoverage.majeure /= teams.length || 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gradient-to-br from-slate-800/40 to-slate-700/30 backdrop-blur-sm border border-slate-600/30 rounded-2xl overflow-hidden shadow-xl">
|
<div className="bg-gradient-to-br from-slate-800/40 to-slate-700/30 backdrop-blur-sm border border-slate-600/30 rounded-2xl overflow-hidden shadow-xl">
|
||||||
<div
|
<div
|
||||||
@@ -153,24 +174,76 @@ export function DirectionOverview({
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
{/* Compétences incontournables */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-slate-300">
|
<span className="text-sm text-slate-300">
|
||||||
Maîtrise globale:
|
Incontournables:
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-bold text-white">
|
<span
|
||||||
{((averageSkillLevel / 3) * 100).toFixed(0)}%
|
className={`text-sm font-bold ${
|
||||||
|
averageCriticalCoverage.incontournable < 75
|
||||||
|
? "text-red-400"
|
||||||
|
: "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{averageCriticalCoverage.incontournable.toFixed(0)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-slate-700/50 rounded-full h-2">
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className="bg-gradient-to-r from-blue-500 to-blue-400 h-2 rounded-full transition-all shadow-sm"
|
className={`h-1.5 rounded-full transition-all shadow-sm ${
|
||||||
style={{ width: `${(averageSkillLevel / 3) * 100}%` }}
|
averageCriticalCoverage.incontournable < 75
|
||||||
|
? "bg-gradient-to-r from-red-500 to-red-400"
|
||||||
|
: "bg-gradient-to-r from-green-500 to-green-400"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
width: `${averageCriticalCoverage.incontournable}%`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2 text-xs text-slate-400">
|
|
||||||
Basé sur {teams.length} équipes
|
{/* Compétences majeures */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-slate-300">Majeures:</span>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-bold ${
|
||||||
|
averageCriticalCoverage.majeure < 60
|
||||||
|
? "text-orange-400"
|
||||||
|
: "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{averageCriticalCoverage.majeure.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className={`h-1.5 rounded-full transition-all shadow-sm ${
|
||||||
|
averageCriticalCoverage.majeure < 60
|
||||||
|
? "bg-gradient-to-r from-orange-500 to-orange-400"
|
||||||
|
: "bg-gradient-to-r from-green-500 to-green-400"
|
||||||
|
}`}
|
||||||
|
style={{ width: `${averageCriticalCoverage.majeure}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Niveau global */}
|
||||||
|
<div className="space-y-2 pt-2 border-t border-slate-600/30">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-slate-300">Niveau global:</span>
|
||||||
|
<span className="text-sm font-bold text-white">
|
||||||
|
{((averageSkillLevel / 3) * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-blue-500 to-blue-400 h-1.5 rounded-full transition-all shadow-sm"
|
||||||
|
style={{ width: `${(averageSkillLevel / 3) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,6 +302,7 @@ export function DirectionOverview({
|
|||||||
averageSkillLevel={team.averageSkillLevel}
|
averageSkillLevel={team.averageSkillLevel}
|
||||||
topSkills={team.topSkills}
|
topSkills={team.topSkills}
|
||||||
skillCoverage={team.skillCoverage}
|
skillCoverage={team.skillCoverage}
|
||||||
|
criticalSkillsCoverage={team.criticalSkillsCoverage}
|
||||||
onViewDetails={() => onViewTeamDetails(team)}
|
onViewDetails={() => onViewTeamDetails(team)}
|
||||||
onViewReport={() => onExportTeamReport(team)}
|
onViewReport={() => onExportTeamReport(team)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,8 +28,14 @@ interface TeamDetailModalProps {
|
|||||||
skillName: string;
|
skillName: string;
|
||||||
averageLevel: number;
|
averageLevel: number;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
|
coverage: number;
|
||||||
}>;
|
}>;
|
||||||
skillCoverage: number;
|
skillCoverage: number;
|
||||||
|
criticalSkillsCoverage: {
|
||||||
|
incontournable: number;
|
||||||
|
majeure: number;
|
||||||
|
};
|
||||||
members: TeamMember[];
|
members: TeamMember[];
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
@@ -113,27 +119,88 @@ export function TeamDetailModal({
|
|||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats générales */}
|
{/* Stats générales */}
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="bg-white/5 border border-white/10 rounded-xl p-4 text-center">
|
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
|
||||||
<Users className="h-6 w-6 text-green-400 mx-auto mb-2" />
|
<h3 className="text-sm font-medium text-slate-300 mb-3">
|
||||||
<div className="text-2xl font-bold text-white">
|
Équipe
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-1 flex items-center gap-3">
|
||||||
|
<Users className="h-5 w-5 text-green-400" />
|
||||||
|
<div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
{team.totalMembers}
|
{team.totalMembers}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-400">Membres</div>
|
<div className="text-xs text-slate-400">Membres</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 border border-white/10 rounded-xl p-4 text-center">
|
</div>
|
||||||
<Eye className="h-6 w-6 text-blue-400 mx-auto mb-2" />
|
<div className="flex-1 flex items-center gap-3">
|
||||||
<div className="text-2xl font-bold text-white">
|
<Eye className="h-5 w-5 text-blue-400" />
|
||||||
|
<div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
{((team.averageSkillLevel / 3) * 100).toFixed(0)}%
|
{((team.averageSkillLevel / 3) * 100).toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-400">Niveau moyen</div>
|
<div className="text-xs text-slate-400">Niveau moyen</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 border border-white/10 rounded-xl p-4 text-center">
|
|
||||||
<ExternalLink className="h-6 w-6 text-orange-400 mx-auto mb-2" />
|
|
||||||
<div className="text-2xl font-bold text-white">
|
|
||||||
{team.skillCoverage.toFixed(0)}%
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-400">Couverture</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
|
||||||
|
<h3 className="text-sm font-medium text-slate-300 mb-3">
|
||||||
|
Couverture
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-slate-300">
|
||||||
|
Incontournables
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-bold ${
|
||||||
|
team.criticalSkillsCoverage.incontournable < 75
|
||||||
|
? "text-red-400"
|
||||||
|
: "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{team.criticalSkillsCoverage.incontournable.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
|
team.criticalSkillsCoverage.incontournable < 75
|
||||||
|
? "bg-red-500"
|
||||||
|
: "bg-green-500"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
width: `${team.criticalSkillsCoverage.incontournable}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-slate-300">Majeures</span>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-bold ${
|
||||||
|
team.criticalSkillsCoverage.majeure < 60
|
||||||
|
? "text-red-400"
|
||||||
|
: "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{team.criticalSkillsCoverage.majeure.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
|
team.criticalSkillsCoverage.majeure < 60
|
||||||
|
? "bg-red-500"
|
||||||
|
: "bg-green-500"
|
||||||
|
}`}
|
||||||
|
style={{ width: `${team.criticalSkillsCoverage.majeure}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -141,16 +208,56 @@ export function TeamDetailModal({
|
|||||||
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
|
<div className="bg-white/5 border border-white/10 rounded-xl p-4">
|
||||||
<h3 className="font-medium text-white mb-3">Top 3 Compétences</h3>
|
<h3 className="font-medium text-white mb-3">Top 3 Compétences</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{team.topSkills.slice(0, 3).map((skill, idx) => (
|
{team.topSkills.slice(0, 3).map((skill, idx) => {
|
||||||
|
const target =
|
||||||
|
skill.importance === "incontournable"
|
||||||
|
? 75
|
||||||
|
: skill.importance === "majeure"
|
||||||
|
? 60
|
||||||
|
: 0;
|
||||||
|
const isUnderTarget = target > 0 && skill.coverage < target;
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="flex items-center justify-between p-2 bg-white/5 rounded-lg"
|
className="flex items-center justify-between p-2 bg-white/5 rounded-lg"
|
||||||
>
|
>
|
||||||
<span className="text-white text-sm">{skill.skillName}</span>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-slate-400">
|
<span className="text-white text-sm">
|
||||||
{((skill.averageLevel / 3) * 100).toFixed(0)}%
|
{skill.skillName}
|
||||||
</span>
|
</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
skill.importance === "incontournable"
|
||||||
|
? "border-red-500/30 text-red-400 text-[10px]"
|
||||||
|
: skill.importance === "majeure"
|
||||||
|
? "border-blue-500/30 text-blue-400 text-[10px]"
|
||||||
|
: "border-slate-500/30 text-slate-400 text-[10px]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{skill.importance === "incontournable"
|
||||||
|
? "Incontournable"
|
||||||
|
: skill.importance === "majeure"
|
||||||
|
? "Majeure"
|
||||||
|
: "Standard"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
className={`text-xs ${
|
||||||
|
isUnderTarget ? "text-red-400" : "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{skill.coverage.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
{target > 0 && (
|
||||||
|
<span className="text-[10px] text-slate-500">
|
||||||
|
(obj. {target}%)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${getSkillLevelColor(
|
className={`w-2 h-2 rounded-full ${getSkillLevelColor(
|
||||||
skill.averageLevel
|
skill.averageLevel
|
||||||
@@ -158,7 +265,8 @@ export function TeamDetailModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import {
|
|||||||
User,
|
User,
|
||||||
Zap,
|
Zap,
|
||||||
Crown,
|
Crown,
|
||||||
|
AlertTriangle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
|
|
||||||
interface TeamStatsRowProps {
|
interface TeamStatsRowProps {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -34,8 +36,14 @@ interface TeamStatsRowProps {
|
|||||||
averageLevel: number;
|
averageLevel: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
|
coverage: number;
|
||||||
}>;
|
}>;
|
||||||
skillCoverage: number;
|
skillCoverage: number;
|
||||||
|
criticalSkillsCoverage: {
|
||||||
|
incontournable: number;
|
||||||
|
majeure: number;
|
||||||
|
};
|
||||||
onViewDetails?: () => void;
|
onViewDetails?: () => void;
|
||||||
onViewReport?: () => void;
|
onViewReport?: () => void;
|
||||||
}
|
}
|
||||||
@@ -77,9 +85,14 @@ export function TeamStatsRow({
|
|||||||
averageSkillLevel,
|
averageSkillLevel,
|
||||||
topSkills,
|
topSkills,
|
||||||
skillCoverage,
|
skillCoverage,
|
||||||
|
criticalSkillsCoverage,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onViewReport,
|
onViewReport,
|
||||||
}: TeamStatsRowProps) {
|
}: TeamStatsRowProps) {
|
||||||
|
// Calculer les alertes sur les compétences critiques
|
||||||
|
const hasIncontournableAlert = criticalSkillsCoverage.incontournable < 75;
|
||||||
|
const hasMajeureAlert = criticalSkillsCoverage.majeure < 60;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group bg-gradient-to-r from-slate-800/50 to-slate-700/40 backdrop-blur-sm border border-slate-600/40 rounded-xl p-4 hover:from-slate-700/60 hover:to-slate-600/50 hover:border-slate-500/50 transition-all duration-300 shadow-lg hover:shadow-xl">
|
<div className="group bg-gradient-to-r from-slate-800/50 to-slate-700/40 backdrop-blur-sm border border-slate-600/40 rounded-xl p-4 hover:from-slate-700/60 hover:to-slate-600/50 hover:border-slate-500/50 transition-all duration-300 shadow-lg hover:shadow-xl">
|
||||||
{/* Layout horizontal compact */}
|
{/* Layout horizontal compact */}
|
||||||
@@ -125,19 +138,57 @@ export function TeamStatsRow({
|
|||||||
|
|
||||||
{/* Indicateurs clés compacts */}
|
{/* Indicateurs clés compacts */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-sm font-bold text-white">
|
<div
|
||||||
{averageSkillLevel.toFixed(1)}
|
className={`text-sm font-bold ${
|
||||||
|
criticalSkillsCoverage.incontournable < 75
|
||||||
|
? "text-red-400"
|
||||||
|
: "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{criticalSkillsCoverage.incontournable.toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-300">/ 3.0</div>
|
<div className="text-xs text-slate-300">Incont.</div>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text-xs">
|
||||||
|
Couverture des compétences incontournables
|
||||||
|
<br />
|
||||||
|
Objectif : 75%
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-sm font-bold text-white">
|
<div
|
||||||
{skillCoverage.toFixed(0)}%
|
className={`text-sm font-bold ${
|
||||||
|
criticalSkillsCoverage.majeure < 60
|
||||||
|
? "text-red-400"
|
||||||
|
: "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{criticalSkillsCoverage.majeure.toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-300">Couv.</div>
|
<div className="text-xs text-slate-300">Maj.</div>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text-xs">
|
||||||
|
Couverture des compétences majeures
|
||||||
|
<br />
|
||||||
|
Objectif : 60%
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -169,21 +220,68 @@ export function TeamStatsRow({
|
|||||||
|
|
||||||
{/* Top skills mini */}
|
{/* Top skills mini */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{topSkills.slice(0, 3).map((skill, idx) => (
|
{topSkills.slice(0, 3).map((skill, idx) => {
|
||||||
|
const colors = getImportanceColors(skill.importance);
|
||||||
|
const target =
|
||||||
|
skill.importance === "incontournable"
|
||||||
|
? 75
|
||||||
|
: skill.importance === "majeure"
|
||||||
|
? 60
|
||||||
|
: 0;
|
||||||
|
const isUnderTarget = target > 0 && skill.coverage < target;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider key={idx}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
key={idx}
|
className={`relative px-2 py-1 border rounded-md text-center min-w-[60px] shadow-sm ${
|
||||||
className={`px-2 py-1 border rounded-md text-center min-w-[60px] shadow-sm ${getSkillLevelBadgeClasses(
|
isUnderTarget
|
||||||
skill.averageLevel
|
? "bg-red-500/20 border-red-500/30"
|
||||||
)}`}
|
: "bg-green-500/20 border-green-500/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`text-xs font-medium truncate mb-1 ${
|
||||||
|
isUnderTarget ? "text-red-400" : "text-green-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-medium text-white truncate mb-1">
|
|
||||||
{skill.skillName}
|
{skill.skillName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs font-bold">
|
<div
|
||||||
{((skill.averageLevel / 3) * 100).toFixed(0)}%
|
className={`text-xs font-bold ${
|
||||||
|
isUnderTarget ? "text-red-400" : "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{skill.coverage.toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="text-xs">
|
||||||
|
<p className="font-medium">{skill.skillName}</p>
|
||||||
|
<p className="text-slate-400">
|
||||||
|
{skill.importance === "incontournable"
|
||||||
|
? "Compétence incontournable"
|
||||||
|
: skill.importance === "majeure"
|
||||||
|
? "Compétence majeure"
|
||||||
|
: "Compétence standard"}
|
||||||
|
</p>
|
||||||
|
{target > 0 && (
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
isUnderTarget ? "text-red-400" : "text-green-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Objectif : {target}%
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions compactes */}
|
{/* Actions compactes */}
|
||||||
|
|||||||
@@ -21,8 +21,18 @@ export interface TeamStats {
|
|||||||
direction: string;
|
direction: string;
|
||||||
totalMembers: number;
|
totalMembers: number;
|
||||||
averageSkillLevel: number;
|
averageSkillLevel: number;
|
||||||
topSkills: Array<{ skillName: string; averageLevel: number; icon?: string }>;
|
topSkills: Array<{
|
||||||
|
skillName: string;
|
||||||
|
averageLevel: number;
|
||||||
|
icon?: string;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
|
coverage: number;
|
||||||
|
}>;
|
||||||
skillCoverage: number; // Percentage of skills evaluated
|
skillCoverage: number; // Percentage of skills evaluated
|
||||||
|
criticalSkillsCoverage: {
|
||||||
|
incontournable: number;
|
||||||
|
majeure: number;
|
||||||
|
};
|
||||||
members: TeamMember[];
|
members: TeamMember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export class AdminService {
|
|||||||
tm.uuid_id,
|
tm.uuid_id,
|
||||||
s.id as skill_id,
|
s.id as skill_id,
|
||||||
s.name as skill_name,
|
s.name as skill_name,
|
||||||
|
s.importance,
|
||||||
sc.name as category_name,
|
sc.name as category_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN se.level = 'never' THEN 0
|
WHEN se.level = 'never' THEN 0
|
||||||
@@ -53,11 +54,34 @@ export class AdminService {
|
|||||||
ss.team_id,
|
ss.team_id,
|
||||||
ss.skill_name,
|
ss.skill_name,
|
||||||
s.icon as skill_icon,
|
s.icon as skill_icon,
|
||||||
AVG(ss.level_numeric) as avg_level
|
s.importance,
|
||||||
|
AVG(ss.level_numeric) as avg_level,
|
||||||
|
COALESCE(
|
||||||
|
SUM(CASE
|
||||||
|
WHEN ss.level_numeric >= 2 THEN 100.0 -- autonomous ou expert
|
||||||
|
WHEN ss.level_numeric = 1 THEN 50.0 -- not-autonomous
|
||||||
|
ELSE 0.0 -- never
|
||||||
|
END) / NULLIF(COUNT(*), 0),
|
||||||
|
0
|
||||||
|
) as coverage
|
||||||
FROM skill_stats ss
|
FROM skill_stats ss
|
||||||
JOIN skills s ON ss.skill_id = s.id
|
JOIN skills s ON ss.skill_id = s.id
|
||||||
WHERE ss.skill_name IS NOT NULL
|
WHERE ss.skill_name IS NOT NULL
|
||||||
GROUP BY ss.team_id, ss.skill_name, s.icon
|
GROUP BY ss.team_id, ss.skill_name, s.icon, s.importance
|
||||||
|
),
|
||||||
|
critical_skills_coverage AS (
|
||||||
|
SELECT
|
||||||
|
team_id,
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN importance = 'incontournable' THEN coverage ELSE NULL END),
|
||||||
|
0
|
||||||
|
) as incontournable_coverage,
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN importance = 'majeure' THEN coverage ELSE NULL END),
|
||||||
|
0
|
||||||
|
) as majeure_coverage
|
||||||
|
FROM team_skill_averages
|
||||||
|
GROUP BY team_id
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
tm.team_id,
|
tm.team_id,
|
||||||
@@ -92,13 +116,26 @@ export class AdminService {
|
|||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'skillName', tsa.skill_name,
|
'skillName', tsa.skill_name,
|
||||||
'averageLevel', tsa.avg_level,
|
'averageLevel', tsa.avg_level,
|
||||||
'icon', tsa.skill_icon
|
'icon', tsa.skill_icon,
|
||||||
) ORDER BY tsa.avg_level DESC
|
'importance', tsa.importance,
|
||||||
|
'coverage', tsa.coverage
|
||||||
|
) ORDER BY
|
||||||
|
CASE tsa.importance
|
||||||
|
WHEN 'incontournable' THEN 2
|
||||||
|
WHEN 'majeure' THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END DESC,
|
||||||
|
tsa.avg_level DESC,
|
||||||
|
tsa.coverage DESC
|
||||||
)
|
)
|
||||||
FROM team_skill_averages tsa
|
FROM team_skill_averages tsa
|
||||||
WHERE tsa.team_id = tm.team_id
|
WHERE tsa.team_id = tm.team_id
|
||||||
LIMIT 3
|
LIMIT 3
|
||||||
) as top_skills,
|
) as top_skills,
|
||||||
|
jsonb_build_object(
|
||||||
|
'incontournable', COALESCE(csc.incontournable_coverage, 0),
|
||||||
|
'majeure', COALESCE(csc.majeure_coverage, 0)
|
||||||
|
) as critical_skills_coverage,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(DISTINCT ss.skill_id) > 0 THEN
|
WHEN COUNT(DISTINCT ss.skill_id) > 0 THEN
|
||||||
(COUNT(DISTINCT ss.skill_id) * 100.0 / (SELECT COUNT(*) FROM skills))
|
(COUNT(DISTINCT ss.skill_id) * 100.0 / (SELECT COUNT(*) FROM skills))
|
||||||
@@ -106,7 +143,8 @@ export class AdminService {
|
|||||||
END as skill_coverage
|
END as skill_coverage
|
||||||
FROM team_members tm
|
FROM team_members tm
|
||||||
LEFT JOIN skill_stats ss ON tm.team_id = ss.team_id AND tm.uuid_id = ss.uuid_id
|
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
|
LEFT JOIN critical_skills_coverage csc ON tm.team_id = csc.team_id
|
||||||
|
GROUP BY tm.team_id, tm.team_name, tm.direction, csc.incontournable_coverage, csc.majeure_coverage
|
||||||
ORDER BY tm.direction, tm.team_name
|
ORDER BY tm.direction, tm.team_name
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -120,6 +158,10 @@ export class AdminService {
|
|||||||
averageSkillLevel: parseFloat(row.avg_skill_level) || 0,
|
averageSkillLevel: parseFloat(row.avg_skill_level) || 0,
|
||||||
topSkills: row.top_skills || [],
|
topSkills: row.top_skills || [],
|
||||||
skillCoverage: parseFloat(row.skill_coverage) || 0,
|
skillCoverage: parseFloat(row.skill_coverage) || 0,
|
||||||
|
criticalSkillsCoverage: row.critical_skills_coverage || {
|
||||||
|
incontournable: 0,
|
||||||
|
majeure: 0,
|
||||||
|
},
|
||||||
members: (row.members || []).filter(
|
members: (row.members || []).filter(
|
||||||
(member: any) => member.uuid !== null
|
(member: any) => member.uuid !== null
|
||||||
),
|
),
|
||||||
@@ -238,14 +280,15 @@ export class AdminService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (parseInt(skillsCheck.rows[0].count) > 0) {
|
if (parseInt(skillsCheck.rows[0].count) > 0) {
|
||||||
throw new Error("Impossible de supprimer une catégorie qui contient des skills");
|
throw new Error(
|
||||||
|
"Impossible de supprimer une catégorie qui contient des skills"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supprimer la catégorie
|
// Supprimer la catégorie
|
||||||
await client.query(
|
await client.query("DELETE FROM skill_categories WHERE id = $1", [
|
||||||
"DELETE FROM skill_categories WHERE id = $1",
|
categoryId,
|
||||||
[categoryId]
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
await client.query("COMMIT");
|
await client.query("COMMIT");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user