261 lines
6.9 KiB
TypeScript
261 lines
6.9 KiB
TypeScript
'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<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: 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;
|
|
}
|