- 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.
201 lines
6.8 KiB
TypeScript
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);
|
|
}
|