import { prisma } from '@/services/core/database'; import { getToday } from '@/lib/date-utils'; type TaskType = { id: string; title: string; description?: string | null; priority: string; // high, medium, low completedAt?: Date | null; createdAt: Date; taskTags?: { tag: { name: string; } }[]; }; type CheckboxType = { id: string; text: string; isChecked: boolean; type: string; // task, meeting date: Date; createdAt: Date; task?: { id: string; title: string; priority: string; taskTags?: { tag: { name: string; } }[]; } | null; }; export interface KeyAccomplishment { id: string; title: string; description?: string; tags: string[]; impact: 'high' | 'medium' | 'low'; completedAt: Date; relatedItems: string[]; // IDs des tâches/checkboxes liées todosCount: number; // Nombre de todos associés } export interface UpcomingChallenge { id: string; title: string; description?: string; tags: string[]; priority: 'high' | 'medium' | 'low'; estimatedEffort: 'days' | 'weeks' | 'hours'; blockers: string[]; deadline?: Date; relatedItems: string[]; // IDs des tâches/checkboxes liées todosCount: number; // Nombre de todos associés } export interface ManagerSummary { period: { start: Date; end: Date; }; keyAccomplishments: KeyAccomplishment[]; upcomingChallenges: UpcomingChallenge[]; metrics: { totalTasksCompleted: number; totalCheckboxesCompleted: number; highPriorityTasksCompleted: number; meetingCheckboxesCompleted: number; completionRate: number; focusAreas: { [category: string]: number }; }; narrative: { weekHighlight: string; mainChallenges: string; nextWeekFocus: string; }; } export class ManagerSummaryService { /** * Génère un résumé orienté manager pour les 7 derniers jours */ static async getManagerSummary(date: Date = getToday()): Promise { // Fenêtre glissante de 7 jours au lieu de semaine calendaire const weekEnd = new Date(date); const weekStart = new Date(date); weekStart.setDate(weekStart.getDate() - 6); // 7 jours en arrière (incluant aujourd'hui) // Récupérer les données de base const [tasks, checkboxes] = await Promise.all([ this.getCompletedTasks(weekStart, weekEnd), this.getCompletedCheckboxes(weekStart, weekEnd) ]); // Analyser et extraire les accomplissements clés const keyAccomplishments = await this.extractKeyAccomplishments(tasks, checkboxes); // Identifier les défis à venir const upcomingChallenges = await this.identifyUpcomingChallenges(); // Calculer les métriques const metrics = this.calculateMetrics(tasks, checkboxes); // Générer le narratif const narrative = this.generateNarrative(keyAccomplishments, upcomingChallenges); return { period: { start: weekStart, end: weekEnd }, keyAccomplishments, upcomingChallenges, metrics, narrative }; } /** * Récupère les tâches complétées de la semaine */ private static async getCompletedTasks(startDate: Date, endDate: Date) { const tasks = await prisma.task.findMany({ where: { OR: [ // Tâches avec completedAt dans la période (priorité) { completedAt: { gte: startDate, lte: endDate } }, // Tâches avec status 'done' et updatedAt dans la période { status: 'done', updatedAt: { gte: startDate, lte: endDate } }, // Tâches avec status 'archived' récemment (aussi des accomplissements) { status: 'archived', updatedAt: { gte: startDate, lte: endDate } } ] }, orderBy: { completedAt: 'desc' }, select: { id: true, title: true, description: true, priority: true, completedAt: true, createdAt: true, taskTags: { select: { tag: { select: { name: true } } } } } }); return tasks; } /** * Récupère les checkboxes complétées de la semaine */ private static async getCompletedCheckboxes(startDate: Date, endDate: Date) { const checkboxes = await prisma.dailyCheckbox.findMany({ where: { isChecked: true, date: { gte: startDate, lte: endDate } }, select: { id: true, text: true, isChecked: true, type: true, date: true, createdAt: true, task: { select: { id: true, title: true, priority: true, taskTags: { select: { tag: { select: { name: true } } } } } } }, orderBy: { date: 'desc' } }); return checkboxes; } /** * Extrait les accomplissements clés basés sur la priorité */ private static async extractKeyAccomplishments(tasks: TaskType[], checkboxes: CheckboxType[]): Promise { const accomplishments: KeyAccomplishment[] = []; // Tâches: prendre toutes les high/medium priority, et quelques low si significatives for (const task of tasks) { const priority = task.priority.toLowerCase(); // Convertir priorité task en impact accomplissement let impact: 'high' | 'medium' | 'low'; if (priority === 'high') { impact = 'high'; } else if (priority === 'medium') { impact = 'medium'; } else { // Pour les low priority, ne garder que si c'est vraiment significatif if (!this.isSignificantTask(task.title)) { continue; } impact = 'low'; } // Compter TOUS les todos associés à cette tâche (pas seulement ceux de la période) // car l'accomplissement c'est la tâche complétée, pas seulement les todos de la période const allRelatedTodos = await prisma.dailyCheckbox.count({ where: { task: { id: task.id } } }); accomplishments.push({ id: `task-${task.id}`, title: task.title, description: task.description || undefined, tags: task.taskTags?.map(tt => tt.tag.name) || [], impact, completedAt: task.completedAt || getToday(), relatedItems: [task.id], todosCount: allRelatedTodos // Nombre total de todos associés à cette tâche }); } // AJOUTER les todos standalone avec la nouvelle règle de priorité // Exclure les todos déjà comptés dans les tâches complétées const standaloneTodos = checkboxes.filter(checkbox => !checkbox.task // Todos non liés à une tâche ); standaloneTodos.forEach(todo => { // Appliquer la nouvelle règle de priorité : // Si pas de tâche associée, priorité faible (même pour les meetings) const impact: 'high' | 'medium' | 'low' = 'low'; accomplishments.push({ id: `todo-${todo.id}`, title: todo.type === 'meeting' ? `📅 ${todo.text}` : todo.text, tags: [], // Todos standalone n'ont pas de tags par défaut impact, completedAt: todo.date, relatedItems: [todo.id], todosCount: 1 // Un todo = 1 todo }); }); // Trier par impact puis par date return accomplishments .sort((a, b) => { const impactOrder = { high: 3, medium: 2, low: 1 }; if (impactOrder[a.impact] !== impactOrder[b.impact]) { return impactOrder[b.impact] - impactOrder[a.impact]; } return b.completedAt.getTime() - a.completedAt.getTime(); }) .slice(0, 12); // Plus d'items maintenant qu'on filtre mieux } /** * Identifie les défis et enjeux à venir */ private static async identifyUpcomingChallenges(): Promise { // Récupérer les tâches à venir (priorité high/medium en premier) const upcomingTasks = await prisma.task.findMany({ where: { completedAt: null }, orderBy: [ { priority: 'asc' }, // high < medium < low { createdAt: 'desc' } ], select: { id: true, title: true, description: true, priority: true, createdAt: true, taskTags: { select: { tag: { select: { name: true } } } } }, take: 30 }); // Récupérer les checkboxes récurrentes non complétées (meetings + tâches prioritaires) const upcomingCheckboxes = await prisma.dailyCheckbox.findMany({ where: { isChecked: false, date: { gte: getToday() }, OR: [ { type: 'meeting' }, { task: { priority: { in: ['high', 'medium'] } } } ] }, select: { id: true, text: true, isChecked: true, type: true, date: true, createdAt: true, task: { select: { id: true, title: true, priority: true, taskTags: { select: { tag: { select: { name: true } } } } } } }, orderBy: [ { date: 'asc' }, { createdAt: 'asc' } ], take: 20 }); const challenges: UpcomingChallenge[] = []; // Analyser les tâches - se baser sur la priorité réelle upcomingTasks.forEach((task) => { const taskPriority = task.priority.toLowerCase(); // Convertir priorité task en priorité challenge let priority: 'high' | 'medium' | 'low'; if (taskPriority === 'high') { priority = 'high'; } else if (taskPriority === 'medium') { priority = 'medium'; } else { // Pour les low priority, ne garder que si c'est vraiment challengeant if (!this.isChallengingTask(task.title)) { return; } priority = 'low'; } const estimatedEffort = this.estimateEffort(task.title, task.description || undefined); // Compter les todos associés à cette tâche const relatedTodos = upcomingCheckboxes.filter(cb => cb.task?.id === task.id); challenges.push({ id: `task-${task.id}`, title: task.title, description: task.description || undefined, tags: task.taskTags?.map(tt => tt.tag.name) || [], priority, estimatedEffort, blockers: this.identifyBlockers(task.title, task.description || undefined), relatedItems: [task.id, ...relatedTodos.map(t => t.id)], todosCount: relatedTodos.length // Nombre réel de todos associés }); }); // Ajouter les todos importants comme challenges upcomingCheckboxes.forEach(checkbox => { // Déterminer la priorité selon la nouvelle règle : // Si le todo est associé à une tâche, prendre la priorité de la tâche // Sinon, priorité faible par défaut (même pour les meetings) let priority: 'high' | 'medium' | 'low'; if (checkbox.task?.priority) { const taskPriority = checkbox.task.priority.toLowerCase(); if (taskPriority === 'high') { priority = 'high'; } else if (taskPriority === 'medium') { priority = 'medium'; } else { priority = 'low'; } } else { // Pas de tâche associée = priorité faible (même pour les meetings) priority = 'low'; } // Inclure tous les todos (toutes priorités, y compris faible, sont maintenant visibles) // La priorité est déterminée par la nouvelle règle et affichée visuellement challenges.push({ id: `checkbox-${checkbox.id}`, title: checkbox.text, tags: checkbox.task?.taskTags?.map(tt => tt.tag.name) || [], priority, estimatedEffort: 'hours', blockers: [], relatedItems: [checkbox.id], todosCount: 1 // Une checkbox = 1 todo }); }); return challenges .sort((a, b) => { const priorityOrder = { high: 3, medium: 2, low: 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }) .slice(0, 10); // Plus d'items maintenant qu'on filtre mieux } /** * Estime l'effort requis */ private static estimateEffort(title: string, description?: string): 'days' | 'weeks' | 'hours' { const content = `${title} ${description || ''}`.toLowerCase(); if (content.includes('architecture') || content.includes('migration') || content.includes('refactor')) { return 'weeks'; } if (content.includes('feature') || content.includes('implement') || content.includes('integration')) { return 'days'; } return 'hours'; } /** * Identifie les blockers potentiels */ private static identifyBlockers(title: string, description?: string): string[] { const content = `${title} ${description || ''}`.toLowerCase(); const blockers: string[] = []; if (content.includes('depends') || content.includes('waiting')) { blockers.push('Dépendances externes'); } if (content.includes('approval') || content.includes('review')) { blockers.push('Validation requise'); } if (content.includes('design') && !content.includes('implement')) { blockers.push('Spécifications incomplètes'); } return blockers; } /** * Détermine si une tâche est significative */ private static isSignificantTask(title: string): boolean { const significantKeywords = [ 'release', 'deploy', 'launch', 'milestone', 'architecture', 'design', 'strategy', 'integration', 'migration', 'optimization' ]; return significantKeywords.some(keyword => title.toLowerCase().includes(keyword)); } /** * Détermine si une checkbox est significative */ private static isSignificantCheckbox(text: string): boolean { const content = text.toLowerCase(); return content.length > 30 || // Checkboxes détaillées content.includes('meeting') || content.includes('review') || content.includes('call') || content.includes('presentation'); } /** * Détermine si une tâche représente un défi */ private static isChallengingTask(title: string): boolean { const challengingKeywords = [ 'complex', 'difficult', 'challenge', 'architecture', 'performance', 'security', 'integration', 'migration', 'optimization' ]; return challengingKeywords.some(keyword => title.toLowerCase().includes(keyword)); } /** * Analyse les patterns dans les checkboxes pour identifier des enjeux */ private static analyzeCheckboxPatterns(): UpcomingChallenge[] { // Pour l'instant, retourner un array vide // À implémenter selon les besoins spécifiques return []; } /** * Calcule les métriques résumées */ private static calculateMetrics(tasks: TaskType[], checkboxes: CheckboxType[]) { const totalTasksCompleted = tasks.length; const totalCheckboxesCompleted = checkboxes.length; // Calculer les métriques détaillées const highPriorityTasksCompleted = tasks.filter(t => t.priority.toLowerCase() === 'high').length; const meetingCheckboxesCompleted = checkboxes.filter(c => c.type === 'meeting').length; // Analyser la répartition par catégorie const focusAreas: { [category: string]: number } = {}; return { totalTasksCompleted, totalCheckboxesCompleted, highPriorityTasksCompleted, meetingCheckboxesCompleted, completionRate: 0, // À calculer par rapport aux objectifs focusAreas }; } /** * Génère le narratif pour le manager */ private static generateNarrative( accomplishments: KeyAccomplishment[], challenges: UpcomingChallenge[] ) { // Points forts des 7 derniers jours const topAccomplishments = accomplishments.slice(0, 3); const weekHighlight = topAccomplishments.length > 0 ? `Ces 7 derniers jours, j'ai principalement progressé sur ${topAccomplishments.map(a => a.title).join(', ')}.` : 'Période focalisée sur l\'exécution des tâches quotidiennes.'; // Défis rencontrés const highImpactItems = accomplishments.filter(a => a.impact === 'high'); const mainChallenges = highImpactItems.length > 0 ? `Les principaux enjeux traités ont été liés aux ${[...new Set(highImpactItems.flatMap(a => a.tags))].join(', ')}.` : 'Pas de blockers majeurs rencontrés sur cette période.'; // Focus 7 prochains jours const topChallenges = challenges.slice(0, 3); const nextWeekFocus = topChallenges.length > 0 ? `Les 7 prochains jours seront concentrés sur ${topChallenges.map(c => c.title).join(', ')}.` : 'Continuation du travail en cours selon les priorités établies.'; return { weekHighlight, mainChallenges, nextWeekFocus }; } }