Files
peakskills/components/team-review/team-overview.tsx
2025-08-27 14:31:05 +02:00

266 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 {
COVERAGE_OBJECTIVES,
isCoverageBelowObjective,
} from "@/lib/evaluation-utils";
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
const uncoveredIncontournables = skillGaps.filter(
(gap) =>
gap.category === cat.category &&
gap.importance === "incontournable" &&
isCoverageBelowObjective(gap.coverage, gap.importance)
);
// 2. Les compétences majeures sous-couvertes
const uncoveredMajeures = skillGaps.filter(
(gap) =>
gap.category === cat.category &&
gap.importance === "majeure" &&
isCoverageBelowObjective(gap.coverage, gap.importance)
);
// 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((cat): cat is NonNullable<typeof cat> => cat !== null)
.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 = COVERAGE_OBJECTIVES[skill.importance];
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={
isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? "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 />
{isCoverageBelowObjective(
skill.coverage,
skill.importance
)
? `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>
);
}