import { prisma } from './database'; import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; import { TaskCategorizationService } from './task-categorization'; export interface DailyItem { id: string; text: string; isChecked: boolean; createdAt: Date; updatedAt: Date; date: Date; } export interface WeeklyStats { totalCheckboxes: number; completedCheckboxes: number; totalTasks: number; completedTasks: number; checkboxCompletionRate: number; taskCompletionRate: number; mostProductiveDay: string; dailyBreakdown: Array<{ date: string; dayName: string; checkboxes: number; completedCheckboxes: number; tasks: number; completedTasks: number; }>; } export interface WeeklyActivity { id: string; type: 'checkbox' | 'task'; title: string; completed: boolean; completedAt?: Date; createdAt: Date; date: string; dayName: string; } export interface VelocityMetrics { currentWeekTasks: number; previousWeekTasks: number; weeklyTrend: number; // Pourcentage d'amélioration/détérioration fourWeekAverage: number; weeklyData: Array<{ weekStart: Date; weekEnd: Date; completedTasks: number; completedCheckboxes: number; totalActivities: number; }>; } export interface PeriodComparison { currentPeriod: { tasks: number; checkboxes: number; total: number; }; previousPeriod: { tasks: number; checkboxes: number; total: number; }; changes: { tasks: number; // pourcentage de changement checkboxes: number; total: number; }; } export interface WeeklySummary { stats: WeeklyStats; activities: WeeklyActivity[]; velocity: VelocityMetrics; categoryBreakdown: { [categoryName: string]: { count: number; percentage: number; color: string; icon: string } }; periodComparison: PeriodComparison | null; period: { start: Date; end: Date; }; } export interface PeriodOption { label: string; days: number; key: string; } export const PERIOD_OPTIONS: PeriodOption[] = [ { label: 'Dernière semaine', days: 7, key: 'week' }, { label: 'Dernières 2 semaines', days: 14, key: '2weeks' }, { label: 'Dernier mois', days: 30, key: 'month' } ]; export class WeeklySummaryService { /** * Récupère le résumé complet de la semaine écoulée */ static async getWeeklySummary(periodDays: number = 7): Promise { const now = new Date(); const startOfPeriod = new Date(now); startOfPeriod.setDate(now.getDate() - periodDays); startOfPeriod.setHours(0, 0, 0, 0); const endOfPeriod = new Date(now); endOfPeriod.setHours(23, 59, 59, 999); console.log(`📊 Génération du résumé (${periodDays} jours) du ${startOfPeriod.toLocaleDateString()} au ${endOfPeriod.toLocaleDateString()}`); const [checkboxes, tasks, velocity] = await Promise.all([ this.getWeeklyCheckboxes(startOfPeriod, endOfPeriod), this.getWeeklyTasks(startOfPeriod, endOfPeriod), this.calculateVelocityMetrics(endOfPeriod) ]); const stats = this.calculateStats(checkboxes, tasks, startOfPeriod, endOfPeriod); const activities = this.mergeActivities(checkboxes, tasks); const categoryBreakdown = this.analyzeCategorization(checkboxes, tasks); // Calculer la comparaison avec la période précédente const periodComparison = await this.calculatePeriodComparison(startOfPeriod, endOfPeriod, periodDays); return { stats, activities, velocity, categoryBreakdown, periodComparison, period: { start: startOfPeriod, end: endOfPeriod } }; } /** * Récupère les checkboxes des 7 derniers jours */ private static async getWeeklyCheckboxes(startDate: Date, endDate: Date): Promise { const items = await prisma.dailyCheckbox.findMany({ where: { date: { gte: startDate, lte: endDate } }, orderBy: [ { date: 'desc' }, { createdAt: 'desc' } ] }); return items.map(item => ({ id: item.id, text: item.text, isChecked: item.isChecked, createdAt: item.createdAt, updatedAt: item.updatedAt, date: item.date })); } /** * Récupère les tâches des 7 derniers jours (créées ou modifiées) */ private static async getWeeklyTasks(startDate: Date, endDate: Date): Promise { const tasks = await prisma.task.findMany({ where: { OR: [ { createdAt: { gte: startDate, lte: endDate } }, { updatedAt: { gte: startDate, lte: endDate } } ] }, orderBy: { updatedAt: 'desc' } }); return tasks.map(task => ({ id: task.id, title: task.title, description: task.description || '', status: task.status as TaskStatus, priority: task.priority as TaskPriority, source: task.source as TaskSource, sourceId: task.sourceId || undefined, createdAt: task.createdAt, updatedAt: task.updatedAt, dueDate: task.dueDate || undefined, completedAt: task.completedAt || undefined, jiraProject: task.jiraProject || undefined, jiraKey: task.jiraKey || undefined, jiraType: task.jiraType || undefined, assignee: task.assignee || undefined, tags: [] // Les tags sont dans une relation séparée, on les laisse vides pour l'instant })); } /** * Calcule les statistiques de la semaine */ private static calculateStats( checkboxes: DailyItem[], tasks: Task[], startDate: Date, endDate: Date ): WeeklyStats { const completedCheckboxes = checkboxes.filter(c => c.isChecked); const completedTasks = tasks.filter(t => t.status === 'done'); // Créer un breakdown par jour const dailyBreakdown = []; const current = new Date(startDate); while (current <= endDate) { const dayCheckboxes = checkboxes.filter(c => c.date.toISOString().split('T')[0] === current.toISOString().split('T')[0] ); const dayCompletedCheckboxes = dayCheckboxes.filter(c => c.isChecked); // Pour les tâches, on compte celles modifiées ce jour-là const dayTasks = tasks.filter(t => t.updatedAt.toISOString().split('T')[0] === current.toISOString().split('T')[0] || t.createdAt.toISOString().split('T')[0] === current.toISOString().split('T')[0] ); const dayCompletedTasks = dayTasks.filter(t => t.status === 'done'); dailyBreakdown.push({ date: current.toISOString().split('T')[0], dayName: current.toLocaleDateString('fr-FR', { weekday: 'long' }), checkboxes: dayCheckboxes.length, completedCheckboxes: dayCompletedCheckboxes.length, tasks: dayTasks.length, completedTasks: dayCompletedTasks.length }); current.setDate(current.getDate() + 1); } // Trouver le jour le plus productif const mostProductiveDay = dailyBreakdown.reduce((max, day) => { const dayScore = day.completedCheckboxes + day.completedTasks; const maxScore = max.completedCheckboxes + max.completedTasks; return dayScore > maxScore ? day : max; }, dailyBreakdown[0]); return { totalCheckboxes: checkboxes.length, completedCheckboxes: completedCheckboxes.length, totalTasks: tasks.length, completedTasks: completedTasks.length, checkboxCompletionRate: checkboxes.length > 0 ? (completedCheckboxes.length / checkboxes.length) * 100 : 0, taskCompletionRate: tasks.length > 0 ? (completedTasks.length / tasks.length) * 100 : 0, mostProductiveDay: mostProductiveDay.dayName, dailyBreakdown }; } /** * Calcule les métriques de vélocité sur 4 semaines */ private static async calculateVelocityMetrics(currentEndDate: Date): Promise { const weeks = []; const currentDate = new Date(currentEndDate); // Générer les 4 dernières semaines for (let i = 0; i < 4; i++) { const weekEnd = new Date(currentDate); weekEnd.setDate(weekEnd.getDate() - (i * 7)); weekEnd.setHours(23, 59, 59, 999); const weekStart = new Date(weekEnd); weekStart.setDate(weekEnd.getDate() - 6); weekStart.setHours(0, 0, 0, 0); const [weekCheckboxes, weekTasks] = await Promise.all([ this.getWeeklyCheckboxes(weekStart, weekEnd), this.getWeeklyTasks(weekStart, weekEnd) ]); const completedTasks = weekTasks.filter(t => t.status === 'done').length; const completedCheckboxes = weekCheckboxes.filter(c => c.isChecked).length; weeks.push({ weekStart, weekEnd, completedTasks, completedCheckboxes, totalActivities: completedTasks + completedCheckboxes }); } // Calculer les métriques const currentWeek = weeks[0]; const previousWeek = weeks[1]; const fourWeekAverage = weeks.reduce((sum, week) => sum + week.totalActivities, 0) / weeks.length; let weeklyTrend = 0; if (previousWeek.totalActivities > 0) { weeklyTrend = ((currentWeek.totalActivities - previousWeek.totalActivities) / previousWeek.totalActivities) * 100; } else if (currentWeek.totalActivities > 0) { weeklyTrend = 100; // 100% d'amélioration si on passe de 0 à quelque chose } return { currentWeekTasks: currentWeek.completedTasks, previousWeekTasks: previousWeek.completedTasks, weeklyTrend, fourWeekAverage, weeklyData: weeks.reverse() // Plus ancien en premier pour l'affichage du graphique }; } /** * Calcule la comparaison avec la période précédente */ private static async calculatePeriodComparison( currentStart: Date, currentEnd: Date, periodDays: number ): Promise { // Période précédente const previousEnd = new Date(currentStart); previousEnd.setHours(23, 59, 59, 999); const previousStart = new Date(previousEnd); previousStart.setDate(previousEnd.getDate() - periodDays); previousStart.setHours(0, 0, 0, 0); const [currentCheckboxes, currentTasks, previousCheckboxes, previousTasks] = await Promise.all([ this.getWeeklyCheckboxes(currentStart, currentEnd), this.getWeeklyTasks(currentStart, currentEnd), this.getWeeklyCheckboxes(previousStart, previousEnd), this.getWeeklyTasks(previousStart, previousEnd) ]); const currentCompletedTasks = currentTasks.filter(t => t.status === 'done').length; const currentCompletedCheckboxes = currentCheckboxes.filter(c => c.isChecked).length; const currentTotal = currentCompletedTasks + currentCompletedCheckboxes; const previousCompletedTasks = previousTasks.filter(t => t.status === 'done').length; const previousCompletedCheckboxes = previousCheckboxes.filter(c => c.isChecked).length; const previousTotal = previousCompletedTasks + previousCompletedCheckboxes; const calculateChange = (current: number, previous: number): number => { if (previous === 0) return current > 0 ? 100 : 0; return ((current - previous) / previous) * 100; }; return { currentPeriod: { tasks: currentCompletedTasks, checkboxes: currentCompletedCheckboxes, total: currentTotal }, previousPeriod: { tasks: previousCompletedTasks, checkboxes: previousCompletedCheckboxes, total: previousTotal }, changes: { tasks: calculateChange(currentCompletedTasks, previousCompletedTasks), checkboxes: calculateChange(currentCompletedCheckboxes, previousCompletedCheckboxes), total: calculateChange(currentTotal, previousTotal) } }; } /** * Analyse la catégorisation des activités */ private static analyzeCategorization(checkboxes: DailyItem[], tasks: Task[]): { [categoryName: string]: { count: number; percentage: number; color: string; icon: string } } { const allActivities = [ ...checkboxes.map(c => ({ title: c.text, description: '' })), ...tasks.map(t => ({ title: t.title, description: t.description || '' })) ]; return TaskCategorizationService.analyzeActivitiesByCategory(allActivities); } /** * Fusionne les activités (checkboxes + tâches) en une timeline */ private static mergeActivities(checkboxes: DailyItem[], tasks: Task[]): WeeklyActivity[] { const activities: WeeklyActivity[] = []; // Ajouter les checkboxes checkboxes.forEach(checkbox => { activities.push({ id: `checkbox-${checkbox.id}`, type: 'checkbox', title: checkbox.text, completed: checkbox.isChecked, completedAt: checkbox.isChecked ? checkbox.updatedAt : undefined, createdAt: checkbox.createdAt, date: checkbox.date.toISOString().split('T')[0], dayName: checkbox.date.toLocaleDateString('fr-FR', { weekday: 'long' }) }); }); // Ajouter les tâches tasks.forEach(task => { const date = task.updatedAt.toISOString().split('T')[0]; const dateObj = new Date(date + 'T00:00:00'); activities.push({ id: `task-${task.id}`, type: 'task', title: task.title, completed: task.status === 'done', completedAt: task.status === 'done' ? task.updatedAt : undefined, createdAt: task.createdAt, date: date, dayName: dateObj.toLocaleDateString('fr-FR', { weekday: 'long' }) }); }); // Trier par date (plus récent en premier) return activities.sort((a, b) => { const dateA = a.completedAt || a.createdAt; const dateB = b.completedAt || b.createdAt; return dateB.getTime() - dateA.getTime(); }); } }