Files
towercontrol/components/jira/TeamActivityHeatmap.tsx
Julien Froidefond 2008cc3382 feat: update Jira components with improved color schemes and UI enhancements
- Refined color schemes in `BurndownChart`, `CollaborationMatrix`, `ThroughputChart`, and `TeamActivityHeatmap` for better visibility and consistency.
- Adjusted opacity handling in `TeamActivityHeatmap` for improved visual clarity.
- Cleaned up imports in `CollaborationMatrix` and `SprintComparison` for better code organization.
- Enhanced `JiraSync` component with updated color for the sync status indicator.
- Updated `jira-period-filter` to remove unused imports, streamlining the codebase.
2025-09-19 08:30:49 +02:00

161 lines
6.6 KiB
TypeScript

'use client';
import { AssigneeWorkload, StatusDistribution } from '@/lib/types';
interface TeamActivityHeatmapProps {
workloadByAssignee: AssigneeWorkload[];
statusDistribution: StatusDistribution[];
className?: string;
}
export function TeamActivityHeatmap({ workloadByAssignee, statusDistribution, className }: TeamActivityHeatmapProps) {
// Calculer l'intensité maximale pour la normalisation
const maxWorkload = Math.max(...workloadByAssignee.map(a => a.totalActive));
// Fonction pour calculer l'intensité de couleur
const getIntensity = (value: number) => {
if (maxWorkload === 0) return 0;
return (value / maxWorkload) * 100;
};
// Couleurs pour les différents types de travail
const getWorkloadColor = (todo: number, inProgress: number, review: number) => {
const total = todo + inProgress + review;
if (total === 0) return null; // Géré séparément
// Dominante par type de travail avec couleurs CSS directes (versions plus douces)
if (review > inProgress && review > todo) {
return '#a855f7'; // purple-500 - Review dominant (plus doux)
} else if (inProgress > todo) {
return '#f59e0b'; // amber-500 - In Progress dominant (plus doux)
} else {
return '#3b82f6'; // blue-500 - Todo dominant (plus doux)
}
};
const getOpacity = (total: number) => {
const intensity = getIntensity(total);
return Math.max(0.6, Math.min(0.9, intensity / 100)); // Opacité plus élevée et moins de variation
};
return (
<div className={className}>
<div className="space-y-4">
{/* Heatmap des assignees */}
<div>
<h4 className="text-sm font-medium mb-3">Intensité de travail par membre</h4>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{workloadByAssignee.map(assignee => {
const bgColor = getWorkloadColor(assignee.todoCount, assignee.inProgressCount, assignee.reviewCount);
const isEmpty = assignee.totalActive === 0;
return (
<div
key={assignee.assignee}
className={`relative p-3 rounded-lg border border-[var(--border)] transition-all hover:scale-105 ${
isEmpty ? 'bg-[var(--muted)]/30' : ''
}`}
style={bgColor ? {
backgroundColor: bgColor,
opacity: getOpacity(assignee.totalActive)
} : {
opacity: getOpacity(assignee.totalActive)
}}
>
<div className={isEmpty ? "text-[var(--foreground)] text-xs font-medium mb-1 truncate" : "text-white text-xs font-medium mb-1 truncate"}>
{assignee.displayName}
</div>
<div className={isEmpty ? "text-[var(--foreground)] text-lg font-bold" : "text-white text-lg font-bold"}>
{assignee.totalActive}
</div>
<div className={isEmpty ? "text-[var(--muted-foreground)] text-xs" : "text-white/80 text-xs"}>
{assignee.todoCount > 0 && `${assignee.todoCount} à faire`}
{assignee.inProgressCount > 0 && ` ${assignee.inProgressCount} en cours`}
{assignee.reviewCount > 0 && ` ${assignee.reviewCount} review`}
</div>
</div>
);
})}
</div>
</div>
{/* Légende */}
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-500 rounded"></div>
<span>À faire dominant</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-amber-500 rounded"></div>
<span>En cours dominant</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-purple-500 rounded"></div>
<span>Review dominant</span>
</div>
<div className="text-[var(--muted-foreground)]">
(Opacité = charge de travail)
</div>
</div>
{/* Matrice de statuts */}
<div>
<h4 className="text-sm font-medium mb-3">Répartition globale par statut</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{statusDistribution.slice(0, 8).map(status => {
const intensity = (status.count / Math.max(...statusDistribution.map(s => s.count))) * 100;
return (
<div
key={status.status}
className="p-3 rounded-lg border border-[var(--border)] bg-gradient-to-r from-[var(--primary)]/20 to-[var(--primary)]"
style={{
backgroundImage: `linear-gradient(to right, var(--primary) ${intensity}%, transparent ${intensity}%)`
}}
>
<div className="text-[var(--foreground)] text-xs font-medium mb-1 truncate">
{status.status}
</div>
<div className="text-[var(--foreground)] text-lg font-bold">
{status.count}
</div>
<div className="text-[var(--foreground)]/80 text-xs">
{status.percentage}%
</div>
</div>
);
})}
</div>
</div>
{/* Métriques rapides */}
<div className="grid grid-cols-3 gap-4 pt-4 border-t border-[var(--border)]">
<div className="text-center">
<div className="text-2xl font-bold text-green-500">
{workloadByAssignee.filter(a => a.totalActive > 0).length}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Membres actifs
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-orange-500">
{workloadByAssignee.reduce((sum, a) => sum + a.totalActive, 0)}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Total WIP
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-blue-500">
{Math.round(workloadByAssignee.reduce((sum, a) => sum + a.totalActive, 0) / Math.max(1, workloadByAssignee.filter(a => a.totalActive > 0).length) * 10) / 10}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
WIP moyen/membre
</div>
</div>
</div>
</div>
</div>
);
}