'use server'; import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integrations/jira/analytics'; import { userPreferencesService } from '@/services/core/user-preferences'; import { SprintDetails } from '@/components/jira/SprintDetailModal'; import { JiraTask, AssigneeDistribution, StatusDistribution, SprintVelocity } from '@/lib/types'; import { parseDate } from '@/lib/date-utils'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; export interface SprintDetailsResult { success: boolean; data?: SprintDetails; error?: string; } /** * Récupère les détails d'un sprint spécifique */ export async function getSprintDetails(sprintName: string): Promise { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } // Récupérer la config Jira const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { return { success: false, error: 'Configuration Jira incomplète' }; } // Récupérer les analytics générales pour trouver le sprint if (!jiraConfig.baseUrl || !jiraConfig.projectKey) { return { success: false, error: 'Configuration Jira incomplète' }; } const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig); const analytics = await analyticsService.getProjectAnalytics(); const sprint = analytics.velocityMetrics.sprintHistory.find(s => s.sprintName === sprintName); if (!sprint) { return { success: false, error: `Sprint "${sprintName}" introuvable` }; } // Récupérer toutes les issues du projet pour filtrer par sprint const allIssues = await analyticsService.getAllProjectIssues(); // Filtrer les issues pour ce sprint spécifique // Note: En réalité, il faudrait une requête JQL plus précise pour récupérer les issues d'un sprint // Pour simplifier, on prend les issues dans la période du sprint const sprintStart = parseDate(sprint.startDate); const sprintEnd = parseDate(sprint.endDate); const sprintIssues = allIssues.filter(issue => { const issueDate = parseDate(issue.created); return issueDate >= sprintStart && issueDate <= sprintEnd; }); // Calculer les métriques du sprint const sprintMetrics = calculateSprintMetrics(sprintIssues, sprint); // Calculer la distribution par assigné pour ce sprint const assigneeDistribution = calculateAssigneeDistribution(sprintIssues); // Calculer la distribution par statut pour ce sprint const statusDistribution = calculateStatusDistribution(sprintIssues); const sprintDetails: SprintDetails = { sprint, issues: sprintIssues, assigneeDistribution, statusDistribution, metrics: sprintMetrics }; return { success: true, data: sprintDetails }; } catch (error) { console.error('❌ Erreur lors de la récupération des détails du sprint:', error); return { success: false, error: error instanceof Error ? error.message : 'Erreur inconnue' }; } } /** * Calcule les métriques spécifiques au sprint */ function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) { const totalIssues = issues.length; const completedIssues = issues.filter(issue => issue.status.category === 'Done' || issue.status.name.toLowerCase().includes('done') || issue.status.name.toLowerCase().includes('closed') ).length; const inProgressIssues = issues.filter(issue => issue.status.category === 'In Progress' || issue.status.name.toLowerCase().includes('progress') || issue.status.name.toLowerCase().includes('review') ).length; const blockedIssues = issues.filter(issue => issue.status.name.toLowerCase().includes('blocked') || issue.status.name.toLowerCase().includes('waiting') ).length; // Calcul du cycle time moyen pour ce sprint const completedIssuesWithDates = issues.filter(issue => issue.status.category === 'Done' && issue.created && issue.updated ); let averageCycleTime = 0; if (completedIssuesWithDates.length > 0) { const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => { const created = parseDate(issue.created); const updated = parseDate(issue.updated); const cycleTime = (updated.getTime() - created.getTime()) / (1000 * 60 * 60 * 24); // en jours return total + cycleTime; }, 0); averageCycleTime = totalCycleTime / completedIssuesWithDates.length; } // Déterminer la tendance de vélocité (simplifié) let velocityTrend: 'up' | 'down' | 'stable' = 'stable'; if (sprint.completedPoints > sprint.plannedPoints * 0.9) { velocityTrend = 'up'; } else if (sprint.completedPoints < sprint.plannedPoints * 0.7) { velocityTrend = 'down'; } return { totalIssues, completedIssues, inProgressIssues, blockedIssues, averageCycleTime, velocityTrend }; } /** * Calcule la distribution par assigné pour le sprint */ function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution[] { const assigneeMap = new Map(); issues.forEach(issue => { const assigneeName = issue.assignee?.displayName || 'Non assigné'; const current = assigneeMap.get(assigneeName) || { total: 0, completed: 0, inProgress: 0 }; current.total++; if (issue.status.category === 'Done') { current.completed++; } else if (issue.status.category === 'In Progress') { current.inProgress++; } assigneeMap.set(assigneeName, current); }); return Array.from(assigneeMap.entries()).map(([displayName, stats]) => ({ assignee: displayName === 'Non assigné' ? '' : displayName, displayName, totalIssues: stats.total, completedIssues: stats.completed, inProgressIssues: stats.inProgress, percentage: issues.length > 0 ? (stats.total / issues.length) * 100 : 0, count: stats.total // Ajout pour compatibilité })).sort((a, b) => b.totalIssues - a.totalIssues); } /** * Calcule la distribution par statut pour le sprint */ function calculateStatusDistribution(issues: JiraTask[]): StatusDistribution[] { const statusMap = new Map(); issues.forEach(issue => { statusMap.set(issue.status.name, (statusMap.get(issue.status.name) || 0) + 1); }); return Array.from(statusMap.entries()).map(([status, count]) => ({ status, count, percentage: issues.length > 0 ? (count / issues.length) * 100 : 0 })).sort((a, b) => b.count - a.count); }