import { prisma } from './database'; import { startOfWeek, endOfWeek, eachDayOfInterval, format, startOfDay, endOfDay } from 'date-fns'; import { fr } from 'date-fns/locale'; export interface DailyMetrics { date: string; // Format ISO dayName: string; // Lundi, Mardi, etc. completed: number; inProgress: number; blocked: number; pending: number; newTasks: number; totalTasks: number; completionRate: number; } export interface VelocityTrend { date: string; completed: number; created: number; velocity: number; } export interface WeeklyMetricsOverview { period: { start: Date; end: Date; }; dailyBreakdown: DailyMetrics[]; summary: { totalTasksCompleted: number; totalTasksCreated: number; averageCompletionRate: number; peakProductivityDay: string; lowProductivityDay: string; trendsAnalysis: { completionTrend: 'improving' | 'declining' | 'stable'; productivityPattern: 'consistent' | 'variable' | 'weekend-heavy'; }; }; statusDistribution: { status: string; count: number; percentage: number; color: string; }[]; priorityBreakdown: { priority: string; completed: number; pending: number; total: number; completionRate: number; color: string; }[]; } export class MetricsService { /** * Récupère les métriques journalières de la semaine */ static async getWeeklyMetrics(date: Date = new Date()): Promise { const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche // Générer tous les jours de la semaine const daysOfWeek = eachDayOfInterval({ start: weekStart, end: weekEnd }); // Récupérer les données pour chaque jour const dailyBreakdown = await Promise.all( daysOfWeek.map(day => this.getDailyMetrics(day)) ); // Calculer les métriques de résumé const summary = this.calculateWeeklySummary(dailyBreakdown); // Récupérer la distribution des statuts pour la semaine const statusDistribution = await this.getStatusDistribution(weekStart, weekEnd); // Récupérer la répartition par priorité const priorityBreakdown = await this.getPriorityBreakdown(weekStart, weekEnd); return { period: { start: weekStart, end: weekEnd }, dailyBreakdown, summary, statusDistribution, priorityBreakdown }; } /** * Récupère les métriques pour un jour donné */ private static async getDailyMetrics(date: Date): Promise { const dayStart = startOfDay(date); const dayEnd = endOfDay(date); // Compter les tâches par statut à la fin de la journée const [completed, inProgress, blocked, pending, newTasks, totalTasks] = await Promise.all([ // Tâches complétées ce jour prisma.task.count({ where: { OR: [ { completedAt: { gte: dayStart, lte: dayEnd } }, { status: 'done', updatedAt: { gte: dayStart, lte: dayEnd } } ] } }), // Tâches en cours (status = in_progress à ce moment) prisma.task.count({ where: { status: 'in_progress', createdAt: { lte: dayEnd } } }), // Tâches bloquées prisma.task.count({ where: { status: 'blocked', createdAt: { lte: dayEnd } } }), // Tâches en attente prisma.task.count({ where: { status: 'pending', createdAt: { lte: dayEnd } } }), // Nouvelles tâches créées ce jour prisma.task.count({ where: { createdAt: { gte: dayStart, lte: dayEnd } } }), // Total des tâches existantes ce jour prisma.task.count({ where: { createdAt: { lte: dayEnd } } }) ]); const completionRate = totalTasks > 0 ? (completed / totalTasks) * 100 : 0; return { date: date.toISOString(), dayName: format(date, 'EEEE', { locale: fr }), completed, inProgress, blocked, pending, newTasks, totalTasks, completionRate: Math.round(completionRate * 100) / 100 }; } /** * Calcule le résumé hebdomadaire */ private static calculateWeeklySummary(dailyBreakdown: DailyMetrics[]) { const totalTasksCompleted = dailyBreakdown.reduce((sum, day) => sum + day.completed, 0); const totalTasksCreated = dailyBreakdown.reduce((sum, day) => sum + day.newTasks, 0); const averageCompletionRate = dailyBreakdown.reduce((sum, day) => sum + day.completionRate, 0) / dailyBreakdown.length; // Identifier les jours de pic et de creux const peakDay = dailyBreakdown.reduce((peak, day) => day.completed > peak.completed ? day : peak ); const lowDay = dailyBreakdown.reduce((low, day) => day.completed < low.completed ? day : low ); // Analyser les tendances const firstHalf = dailyBreakdown.slice(0, 3); const secondHalf = dailyBreakdown.slice(4); const firstHalfAvg = firstHalf.reduce((sum, day) => sum + day.completed, 0) / firstHalf.length; const secondHalfAvg = secondHalf.reduce((sum, day) => sum + day.completed, 0) / secondHalf.length; let completionTrend: 'improving' | 'declining' | 'stable'; if (secondHalfAvg > firstHalfAvg * 1.1) { completionTrend = 'improving'; } else if (secondHalfAvg < firstHalfAvg * 0.9) { completionTrend = 'declining'; } else { completionTrend = 'stable'; } // Analyser le pattern de productivité const weekendDays = dailyBreakdown.slice(5); // Samedi et dimanche const weekdayDays = dailyBreakdown.slice(0, 5); const weekendAvg = weekendDays.reduce((sum, day) => sum + day.completed, 0) / weekendDays.length; const weekdayAvg = weekdayDays.reduce((sum, day) => sum + day.completed, 0) / weekdayDays.length; let productivityPattern: 'consistent' | 'variable' | 'weekend-heavy'; if (weekendAvg > weekdayAvg * 1.2) { productivityPattern = 'weekend-heavy'; } else { const variance = dailyBreakdown.reduce((sum, day) => { const diff = day.completed - (totalTasksCompleted / dailyBreakdown.length); return sum + diff * diff; }, 0) / dailyBreakdown.length; productivityPattern = variance > 4 ? 'variable' : 'consistent'; } return { totalTasksCompleted, totalTasksCreated, averageCompletionRate: Math.round(averageCompletionRate * 100) / 100, peakProductivityDay: peakDay.dayName, lowProductivityDay: lowDay.dayName, trendsAnalysis: { completionTrend, productivityPattern } }; } /** * Récupère la distribution des statuts pour la période */ private static async getStatusDistribution(start: Date, end: Date) { const statusCounts = await prisma.task.groupBy({ by: ['status'], _count: { status: true }, where: { createdAt: { gte: start, lte: end } } }); const total = statusCounts.reduce((sum, item) => sum + item._count.status, 0); const statusColors: { [key: string]: string } = { pending: '#94a3b8', // gray in_progress: '#3b82f6', // blue blocked: '#ef4444', // red done: '#10b981', // green archived: '#6b7280' // gray-500 }; return statusCounts.map(item => ({ status: item.status, count: item._count.status, percentage: Math.round((item._count.status / total) * 100 * 100) / 100, color: statusColors[item.status] || '#6b7280' })); } /** * Récupère la répartition par priorité avec taux de completion */ private static async getPriorityBreakdown(start: Date, end: Date) { const priorities = ['high', 'medium', 'low']; const priorityData = await Promise.all( priorities.map(async (priority) => { const [completed, total] = await Promise.all([ prisma.task.count({ where: { priority, completedAt: { gte: start, lte: end } } }), prisma.task.count({ where: { priority, createdAt: { gte: start, lte: end } } }) ]); const pending = total - completed; const completionRate = total > 0 ? (completed / total) * 100 : 0; return { priority, completed, pending, total, completionRate: Math.round(completionRate * 100) / 100, color: priority === 'high' ? '#ef4444' : priority === 'medium' ? '#f59e0b' : '#10b981' }; }) ); return priorityData; } /** * Récupère les métriques de vélocité d'équipe (pour graphiques de tendance) */ static async getVelocityTrends(weeksBack: number = 4): Promise { const trends = []; for (let i = weeksBack - 1; i >= 0; i--) { const weekStart = startOfWeek(new Date(Date.now() - i * 7 * 24 * 60 * 60 * 1000), { weekStartsOn: 1 }); const weekEnd = endOfWeek(weekStart, { weekStartsOn: 1 }); const [completed, created] = await Promise.all([ prisma.task.count({ where: { completedAt: { gte: weekStart, lte: weekEnd } } }), prisma.task.count({ where: { createdAt: { gte: weekStart, lte: weekEnd } } }) ]); const velocity = created > 0 ? (completed / created) * 100 : 0; trends.push({ date: format(weekStart, 'dd/MM', { locale: fr }), completed, created, velocity: Math.round(velocity * 100) / 100 }); } return trends; } }