Files
towercontrol/components/jira/CollaborationMatrix.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

270 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 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) {
// Analyser les patterns de collaboration basés sur les données existantes
const collaborationData: 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)
};
});
// 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 'bg-green-600 dark:bg-green-500';
case 'medium': return 'bg-yellow-600 dark:bg-yellow-500';
case 'low': return 'bg-gray-500 dark:bg-gray-400';
}
};
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 ${
person.isolation < 30 ? 'bg-green-600 dark:bg-green-500' :
person.isolation < 60 ? 'bg-yellow-600 dark:bg-yellow-500' : 'bg-red-600 dark:bg-red-500'
}`} />
</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 ${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 = ['bg-green-600 dark:bg-green-500', 'bg-blue-600 dark:bg-blue-500', 'bg-yellow-600 dark:bg-yellow-500', 'bg-red-600 dark:bg-red-500'];
return (
<div key={level} className="flex items-center gap-2 text-xs">
<div className={`w-3 h-3 rounded-sm ${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 ${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 text-red-600 dark:text-red-400">
<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 text-green-600 dark:text-green-400">
<span></span>
<span>Excellente collaboration - L&apos;équipe travaille bien ensemble</span>
</div>
)}
{mostIsolated && mostIsolated.isolation > 80 && (
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400">
<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 text-blue-600 dark:text-blue-400">
<span>🔗</span>
<span>Quelques membres travaillent en silo - Organiser des sessions de partage</span>
</div>
)}
</div>
</div>
</div>
);
}