import { prisma } from './database'; import { startOfWeek, endOfWeek } from 'date-fns'; 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 la semaine */ static async getManagerSummary(date: Date = new Date()): Promise { const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche // 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 = 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 extractKeyAccomplishments(tasks: TaskType[], checkboxes: CheckboxType[]): KeyAccomplishment[] { const accomplishments: KeyAccomplishment[] = []; // Tâches: prendre toutes les high/medium priority, et quelques low si significatives tasks.forEach(task => { 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)) { return; } impact = 'low'; } // Compter les todos (checkboxes) associés à cette tâche const relatedTodos = checkboxes.filter(cb => cb.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 || new Date(), relatedItems: [task.id, ...relatedTodos.map(t => t.id)], todosCount: relatedTodos.length // Nombre réel de todos associés }); }); // AJOUTER SEULEMENT les meetings importants standalone (non liés à une tâche) const standaloneMeetings = checkboxes.filter(checkbox => checkbox.type === 'meeting' && !checkbox.task // Meetings non liés à une tâche ); standaloneMeetings.forEach(meeting => { accomplishments.push({ id: `meeting-${meeting.id}`, title: `📅 ${meeting.text}`, tags: [], // Meetings n'ont pas de tags par défaut impact: 'medium', // Meetings sont importants completedAt: meeting.date, relatedItems: [meeting.id], todosCount: 1 // Un meeting = 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: new Date() }, 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); 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], todosCount: 0 // TODO: compter les todos associés à cette tâche }); }); // Ajouter les meetings importants comme challenges upcomingCheckboxes.forEach(checkbox => { if (checkbox.type === 'meeting') { challenges.push({ id: `checkbox-${checkbox.id}`, title: checkbox.text, tags: checkbox.task?.taskTags?.map(tt => tt.tag.name) || [], priority: 'medium', // Meetings sont medium par défaut 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 de la semaine const topAccomplishments = accomplishments.slice(0, 3); const weekHighlight = topAccomplishments.length > 0 ? `Cette semaine, j'ai principalement progressé sur ${topAccomplishments.map(a => a.title).join(', ')}.` : 'Semaine 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 cette semaine.'; // Focus semaine prochaine const topChallenges = challenges.slice(0, 3); const nextWeekFocus = topChallenges.length > 0 ? `La semaine prochaine sera concentrée sur ${topChallenges.map(c => c.title).join(', ')}.` : 'Continuation du travail en cours selon les priorités établies.'; return { weekHighlight, mainChallenges, nextWeekFocus }; } }