feat: enhance Jira dashboard with export functionality and sprint comparison

- Added export options for CSV and JSON in `JiraDashboardPageClient`, allowing users to download metrics easily.
- Integrated `SprintComparison` component to visualize inter-sprint trends and predictions.
- Updated TODO.md to reflect completion of export metrics and sprint comparison features.
This commit is contained in:
Julien Froidefond
2025-09-18 22:43:11 +02:00
parent 10de6d25f7
commit 0acc54025b
5 changed files with 632 additions and 4 deletions

183
src/actions/jira-export.ts Normal file
View File

@@ -0,0 +1,183 @@
'use server';
import { getJiraAnalytics } from './jira-analytics';
export type ExportFormat = 'csv' | 'json';
export type ExportResult = {
success: boolean;
data?: string;
filename?: string;
error?: string;
};
/**
* Server Action pour exporter les analytics Jira au format CSV ou JSON
*/
export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise<ExportResult> {
try {
// Récupérer les analytics (force refresh pour avoir les données les plus récentes)
const analyticsResult = await getJiraAnalytics(true);
if (!analyticsResult.success || !analyticsResult.data) {
return {
success: false,
error: analyticsResult.error || 'Impossible de récupérer les analytics'
};
}
const analytics = analyticsResult.data;
const timestamp = new Date().toISOString().slice(0, 16).replace(/:/g, '-');
const projectKey = analytics.project.key;
if (format === 'json') {
return {
success: true,
data: JSON.stringify(analytics, null, 2),
filename: `jira-analytics-${projectKey}-${timestamp}.json`
};
}
// Format CSV
const csvData = generateCSV(analytics);
return {
success: true,
data: csvData,
filename: `jira-analytics-${projectKey}-${timestamp}.csv`
};
} catch (error) {
console.error('❌ Erreur lors de l\'export des analytics:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Génère un CSV à partir des analytics Jira
*/
function generateCSV(analytics: any): string {
const lines: string[] = [];
// Header du rapport
lines.push('# Rapport Analytics Jira');
lines.push(`# Projet: ${analytics.project.name} (${analytics.project.key})`);
lines.push(`# Généré le: ${new Date().toLocaleString('fr-FR')}`);
lines.push(`# Total tickets: ${analytics.project.totalIssues}`);
lines.push('');
// 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) => {
lines.push([
escapeCsv(assignee.assignee),
escapeCsv(assignee.displayName),
assignee.totalIssues,
assignee.completedIssues,
assignee.inProgressIssues,
assignee.percentage.toFixed(1) + '%'
].join(','));
});
lines.push('');
// 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) => {
lines.push([
escapeCsv(sprint.sprintName),
sprint.startDate.slice(0, 10),
sprint.endDate.slice(0, 10),
sprint.plannedPoints,
sprint.completedPoints,
sprint.completionRate + '%'
].join(','));
});
lines.push('');
// 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) => {
lines.push([
escapeCsv(type.issueType),
type.averageDays,
type.medianDays,
type.samples
].join(','));
});
lines.push('');
// Section 4: Work in Progress
lines.push('## Work in Progress par statut');
lines.push('Statut,Nombre,Pourcentage');
analytics.workInProgress.byStatus.forEach((status: any) => {
lines.push([
escapeCsv(status.status),
status.count,
status.percentage + '%'
].join(','));
});
lines.push('');
// 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) => {
lines.push([
escapeCsv(assignee.assignee),
escapeCsv(assignee.displayName),
assignee.todoCount,
assignee.inProgressCount,
assignee.reviewCount,
assignee.totalActive
].join(','));
});
lines.push('');
// Section 6: Métriques résumé
lines.push('## Métriques de résumé');
lines.push('Métrique,Valeur');
lines.push([
'Total membres équipe',
analytics.teamMetrics.totalAssignees
].join(','));
lines.push([
'Membres actifs',
analytics.teamMetrics.activeAssignees
].join(','));
lines.push([
'Points complétés sprint actuel',
analytics.velocityMetrics.currentSprintPoints
].join(','));
lines.push([
'Vélocité moyenne',
analytics.velocityMetrics.averageVelocity
].join(','));
lines.push([
'Cycle time moyen (jours)',
analytics.cycleTimeMetrics.averageCycleTime
].join(','));
return lines.join('\n');
}
/**
* Échappe les valeurs CSV (guillemets, virgules, retours à la ligne)
*/
function escapeCsv(value: string): string {
if (typeof value !== 'string') return String(value);
// Si la valeur contient des guillemets, virgules ou retours à la ligne
if (value.includes('"') || value.includes(',') || value.includes('\n')) {
// Doubler les guillemets et entourer de guillemets
return '"' + value.replace(/"/g, '""') + '"';
}
return value;
}