- Introduced `CategoryBreakdown`, `JiraWeeklyMetrics`, `PeriodSelector`, and `VelocityMetrics` components to enhance the weekly summary dashboard. - Updated `WeeklySummaryClient` to manage period selection and PDF export functionality. - Enhanced `WeeklySummaryService` to support period comparisons and activity categorization. - Added new API route for fetching weekly summary data based on selected period. - Updated `package.json` and `package-lock.json` to include `jspdf` and related types for PDF generation. - Marked several tasks as complete in `TODO.md` to reflect progress on summary features.
181 lines
6.0 KiB
TypeScript
181 lines
6.0 KiB
TypeScript
import type { JiraConfig } from './jira';
|
|
import { Task } from '@/lib/types';
|
|
|
|
export interface JiraWeeklyMetrics {
|
|
totalJiraTasks: number;
|
|
completedJiraTasks: number;
|
|
totalStoryPoints: number; // Estimation basée sur le type de ticket
|
|
projectsContributed: string[];
|
|
ticketTypes: { [type: string]: number };
|
|
jiraLinks: Array<{
|
|
key: string;
|
|
title: string;
|
|
status: string;
|
|
type: string;
|
|
url: string;
|
|
estimatedPoints: number;
|
|
}>;
|
|
}
|
|
|
|
export class JiraSummaryService {
|
|
/**
|
|
* Enrichit les tâches hebdomadaires avec des métriques Jira
|
|
*/
|
|
static async getJiraWeeklyMetrics(
|
|
weeklyTasks: Task[],
|
|
jiraConfig?: JiraConfig
|
|
): Promise<JiraWeeklyMetrics | null> {
|
|
|
|
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken) {
|
|
return null;
|
|
}
|
|
|
|
const jiraTasks = weeklyTasks.filter(task =>
|
|
task.source === 'jira' && task.jiraKey && task.jiraProject
|
|
);
|
|
|
|
if (jiraTasks.length === 0) {
|
|
return {
|
|
totalJiraTasks: 0,
|
|
completedJiraTasks: 0,
|
|
totalStoryPoints: 0,
|
|
projectsContributed: [],
|
|
ticketTypes: {},
|
|
jiraLinks: []
|
|
};
|
|
}
|
|
|
|
// Calculer les métriques basiques
|
|
const completedJiraTasks = jiraTasks.filter(task => task.status === 'done');
|
|
const projectsContributed = [...new Set(jiraTasks.map(task => task.jiraProject).filter((project): project is string => Boolean(project)))];
|
|
|
|
// Analyser les types de tickets
|
|
const ticketTypes: { [type: string]: number } = {};
|
|
jiraTasks.forEach(task => {
|
|
const type = task.jiraType || 'Unknown';
|
|
ticketTypes[type] = (ticketTypes[type] || 0) + 1;
|
|
});
|
|
|
|
// Estimer les story points basés sur le type de ticket
|
|
const estimateStoryPoints = (type: string): number => {
|
|
const typeMapping: { [key: string]: number } = {
|
|
'Story': 3,
|
|
'Task': 2,
|
|
'Bug': 1,
|
|
'Epic': 8,
|
|
'Sub-task': 1,
|
|
'Improvement': 2,
|
|
'New Feature': 5,
|
|
'défaut': 1, // French
|
|
'amélioration': 2, // French
|
|
'nouvelle fonctionnalité': 5, // French
|
|
};
|
|
|
|
return typeMapping[type] || typeMapping[type?.toLowerCase()] || 2; // Défaut: 2 points
|
|
};
|
|
|
|
const totalStoryPoints = jiraTasks.reduce((sum, task) => {
|
|
return sum + estimateStoryPoints(task.jiraType || '');
|
|
}, 0);
|
|
|
|
// Créer les liens Jira
|
|
const jiraLinks = jiraTasks.map(task => ({
|
|
key: task.jiraKey || '',
|
|
title: task.title,
|
|
status: task.status,
|
|
type: task.jiraType || 'Unknown',
|
|
url: `${jiraConfig.baseUrl.replace('/rest/api/3', '')}/browse/${task.jiraKey}`,
|
|
estimatedPoints: estimateStoryPoints(task.jiraType || '')
|
|
}));
|
|
|
|
return {
|
|
totalJiraTasks: jiraTasks.length,
|
|
completedJiraTasks: completedJiraTasks.length,
|
|
totalStoryPoints,
|
|
projectsContributed,
|
|
ticketTypes,
|
|
jiraLinks
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Récupère la configuration Jira depuis les préférences utilisateur
|
|
*/
|
|
static async getJiraConfig(): Promise<JiraConfig | null> {
|
|
try {
|
|
// Import dynamique pour éviter les cycles de dépendance
|
|
const { userPreferencesService } = await import('./user-preferences');
|
|
const preferences = await userPreferencesService.getAllPreferences();
|
|
|
|
if (!preferences.jiraConfig?.baseUrl ||
|
|
!preferences.jiraConfig?.email ||
|
|
!preferences.jiraConfig?.apiToken) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
baseUrl: preferences.jiraConfig.baseUrl,
|
|
email: preferences.jiraConfig.email,
|
|
apiToken: preferences.jiraConfig.apiToken,
|
|
projectKey: preferences.jiraConfig.projectKey,
|
|
ignoredProjects: preferences.jiraConfig.ignoredProjects
|
|
};
|
|
} catch (error) {
|
|
console.error('Erreur lors de la récupération de la config Jira:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Génère des insights business basés sur les métriques Jira
|
|
*/
|
|
static generateBusinessInsights(jiraMetrics: JiraWeeklyMetrics): string[] {
|
|
const insights: string[] = [];
|
|
|
|
if (jiraMetrics.totalJiraTasks === 0) {
|
|
insights.push("Aucune tâche Jira cette semaine. Concentré sur des tâches internes ?");
|
|
return insights;
|
|
}
|
|
|
|
// Insights sur la completion
|
|
const completionRate = (jiraMetrics.completedJiraTasks / jiraMetrics.totalJiraTasks) * 100;
|
|
if (completionRate >= 80) {
|
|
insights.push(`🎯 Excellent taux de completion Jira: ${completionRate.toFixed(0)}%`);
|
|
} else if (completionRate < 50) {
|
|
insights.push(`⚠️ Taux de completion Jira faible: ${completionRate.toFixed(0)}%. Revoir les estimations ?`);
|
|
}
|
|
|
|
// Insights sur les story points
|
|
if (jiraMetrics.totalStoryPoints > 0) {
|
|
insights.push(`📊 Estimation: ${jiraMetrics.totalStoryPoints} story points traités cette semaine`);
|
|
|
|
const avgPointsPerTask = jiraMetrics.totalStoryPoints / jiraMetrics.totalJiraTasks;
|
|
if (avgPointsPerTask > 4) {
|
|
insights.push(`🏋️ Travail sur des tâches complexes (${avgPointsPerTask.toFixed(1)} pts/tâche en moyenne)`);
|
|
}
|
|
}
|
|
|
|
// Insights sur les projets
|
|
if (jiraMetrics.projectsContributed.length > 1) {
|
|
insights.push(`🤝 Contribution multi-projets: ${jiraMetrics.projectsContributed.join(', ')}`);
|
|
} else if (jiraMetrics.projectsContributed.length === 1) {
|
|
insights.push(`🎯 Focus sur le projet ${jiraMetrics.projectsContributed[0]}`);
|
|
}
|
|
|
|
// Insights sur les types de tickets
|
|
const bugCount = jiraMetrics.ticketTypes['Bug'] || jiraMetrics.ticketTypes['défaut'] || 0;
|
|
const totalTickets = Object.values(jiraMetrics.ticketTypes).reduce((sum, count) => sum + count, 0);
|
|
|
|
if (bugCount > 0) {
|
|
const bugRatio = (bugCount / totalTickets) * 100;
|
|
if (bugRatio > 50) {
|
|
insights.push(`🐛 Semaine focalisée sur la correction de bugs (${bugRatio.toFixed(0)}%)`);
|
|
} else if (bugRatio < 20) {
|
|
insights.push(`✨ Semaine productive avec peu de bugs (${bugRatio.toFixed(0)}%)`);
|
|
}
|
|
}
|
|
|
|
return insights;
|
|
}
|
|
}
|