Files
towercontrol/src/components/jira/CollaborationMatrix.tsx
Julien Froidefond b5d6967fcd feat: refactor theme management and enhance color customization
- Cleaned up theme architecture by consolidating CSS variables and removing redundant theme applications, ensuring a single source of truth for theming.
- Implemented a dark mode override and improved color management using CSS variables for better customization.
- Updated various components to utilize new color variables, enhancing maintainability and visual consistency across the application.
- Added detailed tasks in TODO.md for future enhancements related to user preferences and color customization features.
2025-09-28 10:14:25 +02:00

291 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useEffect, useState } from 'react';
import { JiraAnalytics } from '@/lib/types';
import { Card } from '@/components/ui/Card';
interface CollaborationMatrixProps {
analytics: JiraAnalytics;
className?: string;
}
interface CollaborationData {
assignee: string;
displayName: string;
collaborationScore: number;
dependencies: Array<{
partner: string;
partnerDisplayName: string;
sharedTickets: number;
intensity: 'low' | 'medium' | 'high';
}>;
isolation: number; // Score d'isolation (0-100, plus c'est élevé plus isolé)
}
export function CollaborationMatrix({ analytics, className }: CollaborationMatrixProps) {
const [collaborationData, setCollaborationData] = useState<CollaborationData[]>([]);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
// Analyser les patterns de collaboration basés sur les données existantes
const data: CollaborationData[] = analytics.teamMetrics.issuesDistribution.map(assignee => {
// Simuler des collaborations basées sur les données réelles
const totalTickets = assignee.totalIssues;
// Générer des partenaires de collaboration réalistes
const otherAssignees = analytics.teamMetrics.issuesDistribution.filter(a => a.assignee !== assignee.assignee);
const dependencies = otherAssignees
.slice(0, Math.min(3, otherAssignees.length)) // Maximum 3 collaborations principales
.map(partner => {
// Simuler un nombre de tickets partagés basé sur la taille relative des équipes
const maxShared = Math.min(totalTickets, partner.totalIssues);
const sharedTickets = Math.floor(Math.random() * Math.max(1, maxShared * 0.3));
const intensity: 'low' | 'medium' | 'high' =
sharedTickets > maxShared * 0.2 ? 'high' :
sharedTickets > maxShared * 0.1 ? 'medium' : 'low';
return {
partner: partner.assignee,
partnerDisplayName: partner.displayName,
sharedTickets,
intensity
};
})
.filter(dep => dep.sharedTickets > 0)
.sort((a, b) => b.sharedTickets - a.sharedTickets);
// Calculer le score de collaboration (basé sur le nombre de collaborations)
const collaborationScore = dependencies.reduce((score, dep) => score + dep.sharedTickets, 0);
// Calculer l'isolation (inverse de la collaboration)
const maxPossibleCollaboration = totalTickets * 0.5; // 50% max de collaboration
const isolation = Math.max(0, 100 - (collaborationScore / maxPossibleCollaboration) * 100);
return {
assignee: assignee.assignee,
displayName: assignee.displayName,
collaborationScore,
dependencies,
isolation: Math.round(isolation)
};
});
setCollaborationData(data);
}, [analytics]);
// Ne pas rendre côté serveur pour éviter l'erreur d'hydratation
if (!isClient) {
return (
<div className={className}>
<div className="animate-pulse rounded-lg h-96" style={{ backgroundColor: 'var(--gray-light)' }} />
</div>
);
}
// Statistiques globales
const avgCollaboration = collaborationData.reduce((sum, d) => sum + d.collaborationScore, 0) / collaborationData.length;
const avgIsolation = collaborationData.reduce((sum, d) => sum + d.isolation, 0) / collaborationData.length;
const mostCollaborative = collaborationData.reduce((max, current) =>
current.collaborationScore > max.collaborationScore ? current : max, collaborationData[0]);
const mostIsolated = collaborationData.reduce((max, current) =>
current.isolation > max.isolation ? current : max, collaborationData[0]);
// Couleur d'intensité
const getIntensityColor = (intensity: 'low' | 'medium' | 'high') => {
switch (intensity) {
case 'high': return { backgroundColor: 'var(--green)' };
case 'medium': return { backgroundColor: 'var(--yellow)' };
case 'low': return { backgroundColor: 'var(--gray)' };
}
};
return (
<div className={className}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Matrice de collaboration */}
<div className="lg:col-span-2">
<h4 className="text-sm font-medium mb-3">Réseau de collaboration</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-96 overflow-y-auto">
{collaborationData.map(person => (
<Card key={person.assignee} className="p-3">
<div className="flex items-center justify-between mb-2">
<div className="font-medium text-sm">{person.displayName}</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[var(--muted-foreground)]">
Score: {person.collaborationScore}
</span>
<div
className="w-3 h-3 rounded-full"
style={{
backgroundColor: person.isolation < 30 ? 'var(--green)' :
person.isolation < 60 ? 'var(--yellow)' : 'var(--destructive)'
}}
/>
</div>
</div>
<div className="space-y-1">
{person.dependencies.length > 0 ? (
person.dependencies.map(dep => (
<div key={dep.partner} className="flex items-center justify-between text-xs">
<span className="text-[var(--muted-foreground)] truncate">
{dep.partnerDisplayName}
</span>
<div className="flex items-center gap-2 flex-shrink-0">
<span>{dep.sharedTickets} tickets</span>
<div className="w-2 h-2 rounded-full" style={getIntensityColor(dep.intensity)} />
</div>
</div>
))
) : (
<div className="text-xs text-[var(--muted-foreground)] italic">
Aucune collaboration détectée
</div>
)}
</div>
</Card>
))}
</div>
</div>
{/* Métriques de collaboration */}
<div>
<h4 className="text-sm font-medium mb-3">Analyse d&apos;équipe</h4>
<div className="space-y-4">
{/* Graphique de répartition */}
<Card className="p-3">
<h5 className="text-xs font-medium mb-2">Répartition par niveau</h5>
<div className="space-y-2">
{['Très collaboratif', 'Collaboratif', 'Isolé', 'Très isolé'].map((level, index) => {
const ranges = [[0, 30], [30, 50], [50, 70], [70, 100]];
const [min, max] = ranges[index];
const count = collaborationData.filter(d => d.isolation >= min && d.isolation < max).length;
const colors = ['var(--green)', 'var(--blue)', 'var(--yellow)', 'var(--destructive)'];
return (
<div key={level} className="flex items-center gap-2 text-xs">
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: colors[index] }} />
<span className="flex-1 truncate">{level}</span>
<span className="font-mono text-xs">{count}</span>
</div>
);
})}
</div>
</Card>
{/* Insights */}
<Card className="p-3">
<h5 className="text-xs font-medium mb-2">🏆 Plus collaboratif</h5>
<div className="text-sm">
<div className="font-medium truncate">{mostCollaborative?.displayName}</div>
<div className="text-xs text-[var(--muted-foreground)]">
{mostCollaborative?.collaborationScore} interactions
</div>
</div>
</Card>
<Card className="p-3">
<h5 className="text-xs font-medium mb-2"> Plus isolé</h5>
<div className="text-sm">
<div className="font-medium truncate">{mostIsolated?.displayName}</div>
<div className="text-xs text-[var(--muted-foreground)]">
{mostIsolated?.isolation}% d&apos;isolation
</div>
</div>
</Card>
{/* Légende des intensités */}
<Card className="p-3">
<h5 className="text-xs font-medium mb-2">Légende</h5>
<div className="space-y-1">
{[
{ intensity: 'high' as const, label: 'Forte' },
{ intensity: 'medium' as const, label: 'Modérée' },
{ intensity: 'low' as const, label: 'Faible' }
].map(item => (
<div key={item.intensity} className="flex items-center gap-2 text-xs">
<div className="w-2 h-2 rounded-full" style={getIntensityColor(item.intensity)} />
<span>{item.label}</span>
</div>
))}
</div>
</Card>
</div>
</div>
</div>
{/* Métriques globales */}
<div className="mt-6 grid grid-cols-4 gap-4">
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className="text-lg font-bold text-blue-500">
{Math.round(avgCollaboration)}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Collaboration moyenne
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className={`text-lg font-bold ${avgIsolation < 40 ? 'text-green-500' : avgIsolation < 60 ? 'text-orange-500' : 'text-red-500'}`}>
{Math.round(avgIsolation)}%
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Isolation moyenne
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className="text-lg font-bold text-purple-500">
{collaborationData.filter(d => d.dependencies.length > 0).length}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Membres connectés
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className="text-lg font-bold text-indigo-500">
{collaborationData.reduce((sum, d) => sum + d.dependencies.length, 0)}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Connexions totales
</div>
</div>
</div>
{/* Recommandations */}
<div className="mt-4 p-4 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<h4 className="text-sm font-medium mb-2">Recommandations d&apos;équipe</h4>
<div className="space-y-2 text-sm">
{avgIsolation > 60 && (
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
<span></span>
<span>Isolation élevée - Encourager le pair programming et les reviews croisées</span>
</div>
)}
{avgIsolation < 30 && (
<div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
<span></span>
<span>Excellente collaboration - L&apos;équipe travaille bien ensemble</span>
</div>
)}
{mostIsolated && mostIsolated.isolation > 80 && (
<div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
<span>👥</span>
<span>Attention à {mostIsolated.displayName} - Considérer du mentoring ou du binômage</span>
</div>
)}
{collaborationData.filter(d => d.dependencies.length === 0).length > 0 && (
<div className="flex items-center gap-2" style={{ color: 'var(--blue)' }}>
<span>🔗</span>
<span>Quelques membres travaillent en silo - Organiser des sessions de partage</span>
</div>
)}
</div>
</div>
</div>
);
}