257 lines
10 KiB
TypeScript
257 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { TeamReviewData } from "@/lib/team-review-types";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { AlertTriangle } from "lucide-react";
|
|
import { getImportanceColors } from "@/lib/tech-colors";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
|
|
interface TeamOverviewProps {
|
|
team: TeamReviewData["team"];
|
|
stats: TeamReviewData["stats"];
|
|
members: TeamReviewData["members"];
|
|
categoryCoverage: TeamReviewData["categoryCoverage"];
|
|
skillGaps: TeamReviewData["skillGaps"];
|
|
}
|
|
|
|
export function TeamOverview({
|
|
team,
|
|
stats,
|
|
members,
|
|
categoryCoverage,
|
|
skillGaps,
|
|
}: TeamOverviewProps) {
|
|
// Trouver les top contributeurs
|
|
const topContributors = [...members]
|
|
.sort((a, b) => b.expertSkills - a.expertSkills)
|
|
.slice(0, 3);
|
|
|
|
// Trouver les catégories les plus fortes
|
|
const strongestCategories = [...categoryCoverage]
|
|
.sort((a, b) => b.coverage - a.coverage)
|
|
.slice(0, 2);
|
|
|
|
// Trouver les catégories qui nécessitent de l'attention
|
|
const categoriesNeedingAttention = [...categoryCoverage]
|
|
.map((cat) => {
|
|
// Pour chaque catégorie, on identifie :
|
|
// 1. Les compétences incontournables sous-couvertes (< 75%)
|
|
const uncoveredIncontournables = skillGaps.filter(
|
|
(gap) =>
|
|
gap.category === cat.category &&
|
|
gap.importance === "incontournable" &&
|
|
gap.coverage < 75
|
|
);
|
|
|
|
// 2. Les compétences majeures sous-couvertes (< 60%)
|
|
const uncoveredMajeures = skillGaps.filter(
|
|
(gap) =>
|
|
gap.category === cat.category &&
|
|
gap.importance === "majeure" &&
|
|
gap.coverage < 60
|
|
);
|
|
|
|
// Une catégorie nécessite de l'attention si :
|
|
const needsAttention =
|
|
uncoveredIncontournables.length > 0 || // Il y a des compétences incontournables sous-couvertes
|
|
uncoveredMajeures.length > 0 || // OU des compétences majeures sous-couvertes
|
|
(cat.experts < 2 && stats.totalMembers > 5); // OU pas assez d'experts dans une équipe significative
|
|
|
|
if (!needsAttention) return null;
|
|
|
|
// Calculer un score d'attention pour trier les catégories
|
|
const attentionScore =
|
|
// Les incontournables pèsent plus lourd
|
|
uncoveredIncontournables.length * 3 +
|
|
// Les majeures un peu moins
|
|
uncoveredMajeures.length * 2 +
|
|
// Manque d'experts est un facteur aggravant
|
|
(cat.experts < 2 && stats.totalMembers > 5 ? 1 : 0);
|
|
|
|
return {
|
|
...cat,
|
|
// On combine les deux types pour l'affichage
|
|
criticalSkills: [...uncoveredIncontournables, ...uncoveredMajeures],
|
|
attentionScore,
|
|
};
|
|
})
|
|
.filter(Boolean)
|
|
.sort((a, b) => b.attentionScore - a.attentionScore)
|
|
.slice(0, 3);
|
|
|
|
return (
|
|
<Card className="bg-white/5 border-white/10 backdrop-blur">
|
|
<CardHeader>
|
|
<CardTitle className="text-slate-200">Vue d'ensemble</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
{/* Points forts */}
|
|
<div>
|
|
<h3 className="text-sm font-medium text-slate-200 mb-4">
|
|
Points forts
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{strongestCategories.map((cat) => (
|
|
<div key={cat.category} className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<p className="text-sm font-medium text-slate-300">
|
|
{cat.category}
|
|
</p>
|
|
<span className="text-sm text-slate-400">
|
|
{cat.coverage.toFixed(0)}%
|
|
</span>
|
|
</div>
|
|
<Progress value={cat.coverage} className="bg-white/10" />
|
|
<p className="text-xs text-slate-400">
|
|
{cat.experts} experts • {cat.mentors} mentors
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Top contributeurs */}
|
|
<div>
|
|
<h3 className="text-sm font-medium text-slate-200 mb-4">
|
|
Top contributeurs
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{topContributors.map((member) => (
|
|
<div key={member.member.uuid} className="space-y-1">
|
|
<p className="text-sm font-medium text-slate-300">
|
|
{member.member.firstName} {member.member.lastName}
|
|
</p>
|
|
<div className="flex gap-2 text-xs">
|
|
<Badge
|
|
variant="secondary"
|
|
className="bg-white/10 text-slate-300 border-white/20"
|
|
>
|
|
{member.expertSkills} expertises
|
|
</Badge>
|
|
{member.mentorSkills > 0 && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-slate-300 border-white/20"
|
|
>
|
|
{member.mentorSkills} mentorats
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Points d'attention */}
|
|
<div>
|
|
<h3 className="text-sm font-medium flex items-center gap-2 text-amber-400 mb-4">
|
|
<AlertTriangle size={16} />
|
|
Besoins prioritaires
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{categoriesNeedingAttention.map((cat) => (
|
|
<div key={cat.category} className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<p className="text-sm font-medium text-slate-300">
|
|
{cat.category}
|
|
</p>
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
variant="outline"
|
|
className="text-amber-400 border-amber-400/30"
|
|
>
|
|
{cat.experts} exp • {cat.mentors} ment
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
{/* Compétences sous-couvertes */}
|
|
{cat.criticalSkills.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{cat.criticalSkills
|
|
.sort((a, b) => {
|
|
if (
|
|
a.importance === "incontournable" &&
|
|
b.importance !== "incontournable"
|
|
)
|
|
return -1;
|
|
if (
|
|
a.importance !== "incontournable" &&
|
|
b.importance === "incontournable"
|
|
)
|
|
return 1;
|
|
return a.coverage - b.coverage;
|
|
})
|
|
.map((skill) => {
|
|
const colors = getImportanceColors(skill.importance);
|
|
const target =
|
|
skill.importance === "incontournable" ? 75 : 60;
|
|
return (
|
|
<Tooltip key={skill.skillId}>
|
|
<TooltipTrigger>
|
|
<div
|
|
className={`text-xs rounded-md px-1.5 py-0.5 ${colors.bg} ${colors.border} border flex items-center gap-1.5`}
|
|
>
|
|
<span className={colors.text}>
|
|
{skill.skillName}
|
|
</span>
|
|
<span
|
|
className={
|
|
skill.coverage < target
|
|
? "text-red-400"
|
|
: "text-slate-400"
|
|
}
|
|
>
|
|
{skill.coverage.toFixed(0)}%
|
|
</span>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
|
|
<div className="text-xs">
|
|
<p className="font-medium">
|
|
{skill.importance === "incontournable"
|
|
? "Compétence incontournable"
|
|
: "Compétence majeure"}
|
|
</p>
|
|
<p className="text-slate-400">
|
|
Objectif : {target}% de couverture
|
|
<br />
|
|
Actuel : {skill.coverage.toFixed(0)}%
|
|
<br />
|
|
{skill.coverage < target
|
|
? `Manque ${(
|
|
target - skill.coverage
|
|
).toFixed(0)}%`
|
|
: "Objectif atteint"}
|
|
</p>
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
{stats.learningNeeds > 0 && (
|
|
<div className="pt-2 border-t border-white/10">
|
|
<p className="text-sm text-amber-400 flex items-center gap-2">
|
|
<AlertTriangle size={16} />
|
|
{stats.learningNeeds} compétences sans expert ni mentor
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|