'use server'; import { getJiraAnalytics } from './jira-analytics'; import { formatDateForDisplay, getToday } from '@/lib/date-utils'; export type ExportFormat = 'csv' | 'json'; export type ExportResult = { success: boolean; data?: string; filename?: string; 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 */ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise { 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: JiraAnalytics): 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: ${formatDateForDisplay(getToday(), 'DISPLAY_LONG')}`); 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: AssigneeMetrics) => { 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: SprintHistory) => { 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: CycleTimeByType) => { 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: WorkInProgressStatus) => { 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: WorkInProgressAssignee) => { 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; }