import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; import { prisma } from './database'; import { getToday, parseDate, subtractDays } from '@/lib/date-utils'; export interface ProductivityMetrics { completionTrend: Array<{ date: string; completed: number; created: number; total: number; }>; velocityData: Array<{ week: string; completed: number; average: number; }>; priorityDistribution: Array<{ priority: string; count: number; percentage: number; }>; statusFlow: Array<{ status: string; count: number; percentage: number; }>; weeklyStats: { thisWeek: number; lastWeek: number; change: number; changePercent: number; }; } export interface TimeRange { start: Date; end: Date; } export class AnalyticsService { /** * Calcule les métriques de productivité pour une période donnée */ static async getProductivityMetrics(timeRange?: TimeRange): Promise { try { const now = getToday(); const defaultStart = subtractDays(now, 30); // 30 jours const start = timeRange?.start || defaultStart; const end = timeRange?.end || now; // Récupérer toutes les tâches depuis la base de données avec leurs tags const dbTasks = await prisma.task.findMany({ include: { taskTags: { include: { tag: true } } } }); // Convertir en format Task const tasks: Task[] = dbTasks.map(task => ({ id: task.id, title: task.title, description: task.description || undefined, status: task.status as TaskStatus, priority: task.priority as TaskPriority, source: task.source as TaskSource, sourceId: task.sourceId || undefined, tags: task.taskTags.map(taskTag => taskTag.tag.name), dueDate: task.dueDate || undefined, completedAt: task.completedAt || undefined, createdAt: task.createdAt, updatedAt: task.updatedAt, jiraProject: task.jiraProject || undefined, jiraKey: task.jiraKey || undefined, jiraType: task.jiraType || undefined, assignee: task.assignee || undefined })); return { completionTrend: this.calculateCompletionTrend(tasks, start, end), velocityData: this.calculateVelocity(tasks, start, end), priorityDistribution: this.calculatePriorityDistribution(tasks), statusFlow: this.calculateStatusFlow(tasks), weeklyStats: this.calculateWeeklyStats(tasks) }; } catch (error) { console.error('Erreur lors du calcul des métriques:', error); throw new Error('Impossible de calculer les métriques de productivité'); } } /** * Calcule la tendance de completion des tâches par jour */ private static calculateCompletionTrend(tasks: Task[], start: Date, end: Date) { const trend: Array<{ date: string; completed: number; created: number; total: number }> = []; // Générer les dates pour la période const currentDate = new Date(start.getTime()); while (currentDate <= end) { const dateStr = currentDate.toISOString().split('T')[0]; // Compter les tâches terminées ce jour const completedThisDay = tasks.filter(task => task.completedAt && task.completedAt.toISOString().split('T')[0] === dateStr ).length; // Compter les tâches créées ce jour const createdThisDay = tasks.filter(task => task.createdAt.toISOString().split('T')[0] === dateStr ).length; // Total cumulé jusqu'à ce jour const totalUntilThisDay = tasks.filter(task => task.createdAt <= currentDate ).length; trend.push({ date: dateStr, completed: completedThisDay, created: createdThisDay, total: totalUntilThisDay }); currentDate.setDate(currentDate.getDate() + 1); } return trend; } /** * Calcule la vélocité (tâches terminées par semaine) */ private static calculateVelocity(tasks: Task[], start: Date, end: Date) { const weeklyData: Array<{ week: string; completed: number; average: number }> = []; const completedTasks = tasks.filter(task => task.completedAt); // Grouper par semaine const weekGroups = new Map(); completedTasks.forEach(task => { if (task.completedAt && task.completedAt >= start && task.completedAt <= end) { const weekStart = this.getWeekStart(task.completedAt); const weekKey = weekStart.toISOString().split('T')[0]; weekGroups.set(weekKey, (weekGroups.get(weekKey) || 0) + 1); } }); // Calculer la moyenne mobile const values = Array.from(weekGroups.values()); const average = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; // Convertir en format pour le graphique weekGroups.forEach((count, weekKey) => { const weekDate = parseDate(weekKey); weeklyData.push({ week: `Sem. ${this.getWeekNumber(weekDate)}`, completed: count, average: Math.round(average * 10) / 10 }); }); return weeklyData.sort((a, b) => a.week.localeCompare(b.week)); } /** * Calcule la distribution des priorités */ private static calculatePriorityDistribution(tasks: Task[]) { const priorityCounts = new Map(); const total = tasks.length; tasks.forEach(task => { const priority = task.priority || 'non-définie'; priorityCounts.set(priority, (priorityCounts.get(priority) || 0) + 1); }); return Array.from(priorityCounts.entries()).map(([priority, count]) => ({ priority: this.getPriorityLabel(priority), count, percentage: Math.round((count / total) * 100) })); } /** * Calcule la distribution des statuts */ private static calculateStatusFlow(tasks: Task[]) { const statusCounts = new Map(); const total = tasks.length; tasks.forEach(task => { const status = task.status; statusCounts.set(status, (statusCounts.get(status) || 0) + 1); }); return Array.from(statusCounts.entries()).map(([status, count]) => ({ status: this.getStatusLabel(status), count, percentage: Math.round((count / total) * 100) })); } /** * Calcule les statistiques hebdomadaires */ private static calculateWeeklyStats(tasks: Task[]) { const now = getToday(); const thisWeekStart = this.getWeekStart(now); const lastWeekStart = subtractDays(thisWeekStart, 7); const lastWeekEnd = subtractDays(thisWeekStart, 1); const thisWeekCompleted = tasks.filter(task => task.completedAt && task.completedAt >= thisWeekStart && task.completedAt <= now ).length; const lastWeekCompleted = tasks.filter(task => task.completedAt && task.completedAt >= lastWeekStart && task.completedAt <= lastWeekEnd ).length; const change = thisWeekCompleted - lastWeekCompleted; const changePercent = lastWeekCompleted > 0 ? Math.round((change / lastWeekCompleted) * 100) : thisWeekCompleted > 0 ? 100 : 0; return { thisWeek: thisWeekCompleted, lastWeek: lastWeekCompleted, change, changePercent }; } /** * Obtient le début de la semaine pour une date */ private static getWeekStart(date: Date): Date { const d = new Date(date.getTime()); const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Lundi = début de semaine return new Date(d.setDate(diff)); } /** * Obtient le numéro de la semaine */ private static getWeekNumber(date: Date): number { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); } /** * Convertit le code priorité en label français */ private static getPriorityLabel(priority: string): string { const labels: Record = { 'low': 'Faible', 'medium': 'Moyenne', 'high': 'Élevée', 'urgent': 'Urgente', 'non-définie': 'Non définie' }; return labels[priority] || priority; } /** * Convertit le code statut en label français */ private static getStatusLabel(status: string): string { const labels: Record = { 'backlog': 'Backlog', 'todo': 'À faire', 'in_progress': 'En cours', 'done': 'Terminé', 'cancelled': 'Annulé', 'freeze': 'Gelé', 'archived': 'Archivé' }; return labels[status] || status; } }