Files
towercontrol/src/actions/jira-sprint-details.ts
Julien Froidefond 30aaca4877 feat: enhance user preferences management with userId integration
- Added `userId` field to `UserPreferences` model in Prisma schema for user-specific preferences.
- Implemented migration to populate existing preferences with the first user.
- Updated user preferences service methods to handle user-specific data retrieval and updates.
- Modified API routes and components to ensure user authentication and fetch preferences based on the authenticated user.
- Enhanced session management in various components to load user preferences accordingly.
2025-09-30 22:15:44 +02:00

201 lines
6.8 KiB
TypeScript

'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<SprintDetailsResult> {
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<string, { total: number; completed: number; inProgress: number }>();
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<string, number>();
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);
}