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.
This commit is contained in:
Julien Froidefond
2025-09-19 08:30:49 +02:00
parent 01b702f630
commit 2008cc3382
8 changed files with 132 additions and 56 deletions

View File

@@ -131,12 +131,12 @@ export function BurndownChart({ sprintHistory, className }: BurndownChartProps)
{/* Légende visuelle */}
<div className="mb-4 flex justify-center gap-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-green-500 border-dashed border-t-2 border-green-500"></div>
<span className="text-green-500">Idéal</span>
<div className="w-4 h-0.5 bg-green-600 dark:bg-green-500 border-dashed border-t-2 border-green-600 dark:border-green-500"></div>
<span className="text-green-600 dark:text-green-500">Idéal</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-blue-500"></div>
<span className="text-blue-500">Réel</span>
<div className="w-4 h-0.5 bg-blue-600 dark:bg-blue-500"></div>
<span className="text-blue-600 dark:text-blue-500">Réel</span>
</div>
</div>

View File

@@ -2,7 +2,7 @@
import React from 'react';
import { JiraAnalytics } from '@/lib/types';
import { Card, CardContent, CardHeader } from '@/components/ui/Card';
import { Card } from '@/components/ui/Card';
interface CollaborationMatrixProps {
analytics: JiraAnalytics;
@@ -78,17 +78,9 @@ export function CollaborationMatrix({ analytics, className }: CollaborationMatri
// Couleur d'intensité
const getIntensityColor = (intensity: 'low' | 'medium' | 'high') => {
switch (intensity) {
case 'high': return 'bg-green-500';
case 'medium': return 'bg-yellow-500';
case 'low': return 'bg-gray-400';
}
};
const getIntensityLabel = (intensity: 'low' | 'medium' | 'high') => {
switch (intensity) {
case 'high': return 'Forte';
case 'medium': return 'Modérée';
case 'low': return 'Faible';
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';
}
};
@@ -108,8 +100,8 @@ export function CollaborationMatrix({ analytics, className }: CollaborationMatri
Score: {person.collaborationScore}
</span>
<div className={`w-3 h-3 rounded-full ${
person.isolation < 30 ? 'bg-green-500' :
person.isolation < 60 ? 'bg-yellow-500' : 'bg-red-500'
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>
@@ -149,8 +141,7 @@ export function CollaborationMatrix({ analytics, className }: CollaborationMatri
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 percentage = (count / collaborationData.length) * 100;
const colors = ['bg-green-500', 'bg-blue-500', 'bg-yellow-500', 'bg-red-500'];
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">

View File

@@ -149,7 +149,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse"></div>
<div className="w-2 h-2 rounded-full bg-blue-500 dark:bg-blue-400 animate-pulse"></div>
<h3 className="font-mono text-sm font-bold text-blue-400 uppercase tracking-wider">
JIRA SYNC
</h3>

View File

@@ -1,8 +1,8 @@
'use client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } from 'recharts';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
import { SprintVelocity } from '@/lib/types';
import { Card, CardContent, CardHeader } from '@/components/ui/Card';
import { Card } from '@/components/ui/Card';
interface SprintComparisonProps {
sprintHistory: SprintVelocity[];

View File

@@ -21,21 +21,21 @@ export function TeamActivityHeatmap({ workloadByAssignee, statusDistribution, cl
// 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 'bg-[var(--muted)]/20';
if (total === 0) return null; // Géré séparément
// Dominante par type de travail
// Dominante par type de travail avec couleurs CSS directes (versions plus douces)
if (review > inProgress && review > todo) {
return 'bg-purple-500'; // Review dominant
return '#a855f7'; // purple-500 - Review dominant (plus doux)
} else if (inProgress > todo) {
return 'bg-orange-500'; // In Progress dominant
return '#f59e0b'; // amber-500 - In Progress dominant (plus doux)
} else {
return 'bg-blue-500'; // Todo dominant
return '#3b82f6'; // blue-500 - Todo dominant (plus doux)
}
};
const getOpacity = (total: number) => {
const intensity = getIntensity(total);
return Math.max(0.2, intensity / 100);
return Math.max(0.6, Math.min(0.9, intensity / 100)); // Opacité plus élevée et moins de variation
};
return (
@@ -45,28 +45,37 @@ export function TeamActivityHeatmap({ workloadByAssignee, statusDistribution, cl
<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 => (
<div
key={assignee.assignee}
className="relative p-3 rounded-lg border border-[var(--border)] transition-all hover:scale-105"
style={{
backgroundColor: getWorkloadColor(assignee.todoCount, assignee.inProgressCount, assignee.reviewCount),
opacity: getOpacity(assignee.totalActive)
}}
>
<div className="text-white text-xs font-medium mb-1 truncate">
{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="text-white text-lg font-bold">
<div className={isEmpty ? "text-[var(--foreground)] text-lg font-bold" : "text-white text-lg font-bold"}>
{assignee.totalActive}
</div>
<div className="text-white/80 text-xs">
<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>
@@ -77,7 +86,7 @@ export function TeamActivityHeatmap({ workloadByAssignee, statusDistribution, cl
<span>À faire dominant</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-orange-500 rounded"></div>
<div className="w-3 h-3 bg-amber-500 rounded"></div>
<span>En cours dominant</span>
</div>
<div className="flex items-center gap-2">

View File

@@ -138,16 +138,16 @@ export function ThroughputChart({ sprintHistory, className }: ThroughputChartPro
{/* Légende visuelle */}
<div className="mb-4 flex justify-center gap-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-4 h-3 bg-blue-500 rounded-sm"></div>
<span className="text-blue-500">Points complétés</span>
<div className="w-4 h-3 bg-blue-600 dark:bg-blue-500 rounded-sm"></div>
<span className="text-blue-600 dark:text-blue-500">Points complétés</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-green-500"></div>
<span className="text-green-500">Throughput</span>
<div className="w-4 h-0.5 bg-green-600 dark:bg-green-500"></div>
<span className="text-green-600 dark:text-green-500">Throughput</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-orange-500 border-dashed border-t-2 border-orange-500"></div>
<span className="text-orange-500">Tendance</span>
<div className="w-4 h-0.5 bg-orange-600 dark:bg-orange-500 border-dashed border-t-2 border-orange-600 dark:border-orange-500"></div>
<span className="text-orange-600 dark:text-orange-500">Tendance</span>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { JiraAnalytics, JiraTask, SprintVelocity } from './types';
import { JiraAnalytics } from './types';
export type PeriodFilter = '7d' | '30d' | '3m' | 'current';

View File

@@ -11,6 +11,82 @@ export type ExportResult = {
error?: string;
};
export interface JiraProjectMetrics {
key: string;
name: string;
totalIssues: number;
}
export interface AssigneeMetrics {
assignee: string;
displayName: string;
totalIssues: number;
completedIssues: number;
inProgressIssues: number;
percentage: number;
}
export interface TeamMetrics {
issuesDistribution: AssigneeMetrics[];
totalAssignees: number;
activeAssignees: number;
}
export interface SprintHistory {
sprintName: string;
startDate: string;
endDate: string;
plannedPoints: number;
completedPoints: number;
completionRate: number;
}
export interface VelocityMetrics {
sprintHistory: SprintHistory[];
currentSprintPoints: number;
averageVelocity: number;
}
export interface CycleTimeByType {
issueType: string;
averageDays: number;
medianDays: number;
samples: number;
}
export interface CycleTimeMetrics {
cycleTimeByType: CycleTimeByType[];
averageCycleTime: number;
}
export interface WorkInProgressStatus {
status: string;
count: number;
percentage: number;
}
export interface WorkInProgressAssignee {
assignee: string;
displayName: string;
todoCount: number;
inProgressCount: number;
reviewCount: number;
totalActive: number;
}
export interface WorkInProgress {
byStatus: WorkInProgressStatus[];
byAssignee: WorkInProgressAssignee[];
}
export interface JiraAnalytics {
project: JiraProjectMetrics;
teamMetrics: TeamMetrics;
velocityMetrics: VelocityMetrics;
cycleTimeMetrics: CycleTimeMetrics;
workInProgress: WorkInProgress;
}
/**
* Server Action pour exporter les analytics Jira au format CSV ou JSON
*/
@@ -60,7 +136,7 @@ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise
/**
* Génère un CSV à partir des analytics Jira
*/
function generateCSV(analytics: any): string {
function generateCSV(analytics: JiraAnalytics): string {
const lines: string[] = [];
// Header du rapport
@@ -73,7 +149,7 @@ function generateCSV(analytics: any): string {
// Section 1: Métriques d'équipe
lines.push('## Répartition de l\'équipe');
lines.push('Assignee,Nom,Total Tickets,Tickets Complétés,Tickets En Cours,Pourcentage');
analytics.teamMetrics.issuesDistribution.forEach((assignee: any) => {
analytics.teamMetrics.issuesDistribution.forEach((assignee: AssigneeMetrics) => {
lines.push([
escapeCsv(assignee.assignee),
escapeCsv(assignee.displayName),
@@ -88,7 +164,7 @@ function generateCSV(analytics: any): string {
// Section 2: Historique des sprints
lines.push('## Historique des sprints');
lines.push('Sprint,Date Début,Date Fin,Points Planifiés,Points Complétés,Taux de Complétion');
analytics.velocityMetrics.sprintHistory.forEach((sprint: any) => {
analytics.velocityMetrics.sprintHistory.forEach((sprint: SprintHistory) => {
lines.push([
escapeCsv(sprint.sprintName),
sprint.startDate.slice(0, 10),
@@ -103,7 +179,7 @@ function generateCSV(analytics: any): string {
// Section 3: Cycle time par type
lines.push('## Cycle Time par type de ticket');
lines.push('Type de Ticket,Temps Moyen (jours),Temps Médian (jours),Échantillons');
analytics.cycleTimeMetrics.cycleTimeByType.forEach((type: any) => {
analytics.cycleTimeMetrics.cycleTimeByType.forEach((type: CycleTimeByType) => {
lines.push([
escapeCsv(type.issueType),
type.averageDays,
@@ -116,7 +192,7 @@ function generateCSV(analytics: any): string {
// Section 4: Work in Progress
lines.push('## Work in Progress par statut');
lines.push('Statut,Nombre,Pourcentage');
analytics.workInProgress.byStatus.forEach((status: any) => {
analytics.workInProgress.byStatus.forEach((status: WorkInProgressStatus) => {
lines.push([
escapeCsv(status.status),
status.count,
@@ -128,7 +204,7 @@ function generateCSV(analytics: any): string {
// Section 5: Charge de travail par assignee
lines.push('## Charge de travail par assignee');
lines.push('Assignee,Nom,À Faire,En Cours,En Revue,Total Actif');
analytics.workInProgress.byAssignee.forEach((assignee: any) => {
analytics.workInProgress.byAssignee.forEach((assignee: WorkInProgressAssignee) => {
lines.push([
escapeCsv(assignee.assignee),
escapeCsv(assignee.displayName),