From fd3827214fd483a9dab01f824d7d43ed69b7e317 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Tue, 23 Sep 2025 21:22:59 +0200 Subject: [PATCH] feat: update dashboard components and analytics for 7-day summaries - Modified `ManagerWeeklySummary`, `MetricsTab`, and `ProductivityAnalytics` to reflect a focus on the last 7 days instead of the current week. - Enhanced `ManagerSummaryService` and `MetricsService` to calculate metrics over a sliding 7-day window, improving data relevance. - Added a new utility function `formatDistanceToNow` for better date formatting in French. - Updated comments and documentation to clarify changes in timeframes. --- src/actions/deadline-analytics.ts | 29 +++ .../dashboard/ManagerWeeklySummary.tsx | 6 +- src/components/dashboard/MetricsTab.tsx | 2 +- .../dashboard/ProductivityAnalytics.tsx | 6 +- .../deadline/CriticalDeadlinesCard.tsx | 169 ++++++++++++++ src/components/deadline/DeadlineOverview.tsx | 89 ++++++++ src/components/deadline/DeadlineRiskCard.tsx | 89 ++++++++ .../deadline/DeadlineSummaryCard.tsx | 91 ++++++++ src/components/deadline/index.ts | 4 + src/lib/date-utils.ts | 16 +- src/services/analytics/analytics.ts | 32 ++- src/services/analytics/deadline-analytics.ts | 206 ++++++++++++++++++ src/services/analytics/manager-summary.ts | 21 +- src/services/analytics/metrics.ts | 10 +- 14 files changed, 738 insertions(+), 32 deletions(-) create mode 100644 src/actions/deadline-analytics.ts create mode 100644 src/components/deadline/CriticalDeadlinesCard.tsx create mode 100644 src/components/deadline/DeadlineOverview.tsx create mode 100644 src/components/deadline/DeadlineRiskCard.tsx create mode 100644 src/components/deadline/DeadlineSummaryCard.tsx create mode 100644 src/components/deadline/index.ts create mode 100644 src/services/analytics/deadline-analytics.ts diff --git a/src/actions/deadline-analytics.ts b/src/actions/deadline-analytics.ts new file mode 100644 index 0000000..7f27bbb --- /dev/null +++ b/src/actions/deadline-analytics.ts @@ -0,0 +1,29 @@ +'use server'; + +import { DeadlineAnalyticsService, DeadlineMetrics } from '@/services/analytics/deadline-analytics'; + +export async function getDeadlineMetrics() { + try { + const metrics = await DeadlineAnalyticsService.getDeadlineMetrics(); + return { success: true, data: metrics }; + } catch (error) { + console.error('Erreur lors de la récupération des métriques d\'échéance:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur lors de la récupération des métriques d\'échéance' + }; + } +} + +export async function getCriticalDeadlines() { + try { + const tasks = await DeadlineAnalyticsService.getCriticalDeadlines(); + return { success: true, data: tasks }; + } catch (error) { + console.error('Erreur lors de la récupération des échéances critiques:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur lors de la récupération des échéances critiques' + }; + } +} diff --git a/src/components/dashboard/ManagerWeeklySummary.tsx b/src/components/dashboard/ManagerWeeklySummary.tsx index 440c890..2d96033 100644 --- a/src/components/dashboard/ManagerWeeklySummary.tsx +++ b/src/components/dashboard/ManagerWeeklySummary.tsx @@ -27,7 +27,7 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu const formatPeriod = () => { - return `Semaine du ${format(summary.period.start, 'dd MMM', { locale: fr })} au ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })}`; + return `7 derniers jours (${format(summary.period.start, 'dd MMM', { locale: fr })} - ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })})`; }; const getPriorityBadgeStyle = (priority: 'low' | 'medium' | 'high') => { @@ -134,7 +134,7 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
-

🔮 Focus semaine prochaine

+

🔮 Focus 7 prochains jours

{summary.narrative.nextWeekFocus}

@@ -346,7 +346,7 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu {activeView === 'accomplishments' && ( -

✅ Accomplissements de la semaine

+

✅ Accomplissements des 7 derniers jours

{summary.keyAccomplishments.length} accomplissements significatifs • {summary.metrics.totalTasksCompleted} tâches • {summary.metrics.totalCheckboxesCompleted} todos complétés

diff --git a/src/components/dashboard/MetricsTab.tsx b/src/components/dashboard/MetricsTab.tsx index 709fa8b..15d5115 100644 --- a/src/components/dashboard/MetricsTab.tsx +++ b/src/components/dashboard/MetricsTab.tsx @@ -31,7 +31,7 @@ export function MetricsTab({ className }: MetricsTabProps) { const formatPeriod = () => { if (!metrics) return ''; - return `Semaine du ${format(metrics.period.start, 'dd MMM', { locale: fr })} au ${format(metrics.period.end, 'dd MMM yyyy', { locale: fr })}`; + return `7 derniers jours (${format(metrics.period.start, 'dd MMM', { locale: fr })} - ${format(metrics.period.end, 'dd MMM yyyy', { locale: fr })})`; }; diff --git a/src/components/dashboard/ProductivityAnalytics.tsx b/src/components/dashboard/ProductivityAnalytics.tsx index 6a962d4..8560826 100644 --- a/src/components/dashboard/ProductivityAnalytics.tsx +++ b/src/components/dashboard/ProductivityAnalytics.tsx @@ -8,6 +8,7 @@ import { VelocityChart } from '@/components/charts/VelocityChart'; import { PriorityDistributionChart } from '@/components/charts/PriorityDistributionChart'; import { WeeklyStatsCard } from '@/components/charts/WeeklyStatsCard'; import { Card } from '@/components/ui/Card'; +import { DeadlineOverview } from '@/components/deadline/DeadlineOverview'; export function ProductivityAnalytics() { const [metrics, setMetrics] = useState(null); @@ -67,7 +68,10 @@ export function ProductivityAnalytics() { return (
- {/* Titre de section */} + {/* Section Échéances Critiques */} + + + {/* Titre de section Analytics */}

📊 Analytics & Métriques

diff --git a/src/components/deadline/CriticalDeadlinesCard.tsx b/src/components/deadline/CriticalDeadlinesCard.tsx new file mode 100644 index 0000000..9f91405 --- /dev/null +++ b/src/components/deadline/CriticalDeadlinesCard.tsx @@ -0,0 +1,169 @@ +'use client'; + +import { DeadlineTask } from '@/services/analytics/deadline-analytics'; +import { Card } from '@/components/ui/Card'; + +interface CriticalDeadlinesCardProps { + overdue: DeadlineTask[]; + critical: DeadlineTask[]; + warning: DeadlineTask[]; +} + +export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDeadlinesCardProps) { + // Combiner toutes les tâches urgentes et trier par urgence + const urgentTasks = [...overdue, ...critical, ...warning] + .sort((a, b) => { + // En retard d'abord, puis critique, puis attention + const urgencyOrder: Record = { 'overdue': 0, 'critical': 1, 'warning': 2 }; + if (urgencyOrder[a.urgencyLevel] !== urgencyOrder[b.urgencyLevel]) { + return urgencyOrder[a.urgencyLevel] - urgencyOrder[b.urgencyLevel]; + } + // Si même urgence, trier par jours restants + return a.daysRemaining - b.daysRemaining; + }); + + const getUrgencyStyle = (task: DeadlineTask) => { + if (task.urgencyLevel === 'overdue') { + return { + icon: '🔴', + text: task.daysRemaining === -1 ? 'En retard de 1 jour' : `En retard de ${Math.abs(task.daysRemaining)} jours`, + style: 'text-red-700 bg-red-50/40 border-red-200/60 dark:bg-red-950/20 dark:border-red-800/40 dark:text-red-300' + }; + } else if (task.urgencyLevel === 'critical') { + return { + icon: '🟠', + text: task.daysRemaining === 0 ? 'Échéance aujourd\'hui' : + task.daysRemaining === 1 ? 'Échéance demain' : + `Dans ${task.daysRemaining} jours`, + style: 'text-orange-700 bg-orange-50/40 border-orange-200/60 dark:bg-orange-950/20 dark:border-orange-800/40 dark:text-orange-300' + }; + } else { + return { + icon: '🟡', + text: `Dans ${task.daysRemaining} jours`, + style: 'text-yellow-700 bg-yellow-50/40 border-yellow-200/60 dark:bg-yellow-950/20 dark:border-yellow-800/40 dark:text-yellow-300' + }; + } + }; + + const getPriorityIcon = (priority: string) => { + switch (priority) { + case 'urgent': return '🔥'; + case 'high': return '⬆️'; + case 'medium': return '➡️'; + case 'low': return '⬇️'; + default: return '❓'; + } + }; + + const getSourceIcon = (source: string) => { + switch (source.toLowerCase()) { + case 'jira': return '🔵'; + case 'tfs': return '🟣'; + case 'manual': return '✏️'; + default: return '📋'; + } + }; + + if (urgentTasks.length === 0) { + return ( + +

Tâches Urgentes

+
+
🎉
+

Excellent !

+

+ Aucune tâche urgente ou critique +

+
+
+ ); + } + + return ( + +
+

Tâches Urgentes

+
+ {urgentTasks.length} tâche{urgentTasks.length > 1 ? 's' : ''} +
+
+ +
+ {urgentTasks.map((task) => { + const urgencyStyle = getUrgencyStyle(task); + + return ( +
+
+
+
+ {urgencyStyle.icon} + {getSourceIcon(task.source)} + {getPriorityIcon(task.priority)} + {task.jiraKey && ( + + {task.jiraKey} + + )} +
+ +

+ {task.title} +

+ +
+ {urgencyStyle.text} +
+ + {task.tags.length > 0 && ( +
+ {task.tags.slice(0, 2).map((tag, index) => ( + + {tag} + + ))} + {task.tags.length > 2 && ( + + +{task.tags.length - 2} + + )} +
+ )} +
+
+
+ ); + })} +
+ + {urgentTasks.length > 0 && ( +
+
+ {overdue.length > 0 && ( + + {overdue.length} en retard + + )} + {critical.length > 0 && ( + + {critical.length} critique{critical.length > 1 ? 's' : ''} + + )} + {warning.length > 0 && ( + + {warning.length} attention + + )} +
+
+ )} +
+ ); +} diff --git a/src/components/deadline/DeadlineOverview.tsx b/src/components/deadline/DeadlineOverview.tsx new file mode 100644 index 0000000..bb406cd --- /dev/null +++ b/src/components/deadline/DeadlineOverview.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { useState, useEffect, useTransition } from 'react'; +import { DeadlineMetrics } from '@/services/analytics/deadline-analytics'; +import { getDeadlineMetrics } from '@/actions/deadline-analytics'; +import { Card } from '@/components/ui/Card'; +import { DeadlineRiskCard } from './DeadlineRiskCard'; +import { CriticalDeadlinesCard } from './CriticalDeadlinesCard'; +import { DeadlineSummaryCard } from './DeadlineSummaryCard'; + +export function DeadlineOverview() { + const [metrics, setMetrics] = useState(null); + const [error, setError] = useState(null); + const [isPending, startTransition] = useTransition(); + + useEffect(() => { + const loadMetrics = () => { + startTransition(async () => { + try { + setError(null); + const response = await getDeadlineMetrics(); + + if (response.success && response.data) { + setMetrics(response.data); + } else { + setError(response.error || 'Erreur lors du chargement des échéances'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Erreur lors du chargement des échéances'); + console.error('Erreur échéances:', err); + } + }); + }; + + loadMetrics(); + }, []); + + if (isPending) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + +
+
+
+ ))} +
+ ); + } + + if (error) { + return ( + +
+
⚠️
+

Erreur de chargement des échéances

+

{error}

+
+
+ ); + } + + if (!metrics) { + return null; + } + + return ( +
+ {/* Titre de section */} +
+

🚨 Échéances Critiques

+
+ Surveillance temps réel +
+
+ + {/* Cards principales */} +
+ + + +
+
+ ); +} diff --git a/src/components/deadline/DeadlineRiskCard.tsx b/src/components/deadline/DeadlineRiskCard.tsx new file mode 100644 index 0000000..cd77c23 --- /dev/null +++ b/src/components/deadline/DeadlineRiskCard.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { DeadlineMetrics, DeadlineAnalyticsService } from '@/services/analytics/deadline-analytics'; +import { Card } from '@/components/ui/Card'; + +interface DeadlineRiskCardProps { + metrics: DeadlineMetrics; +} + +export function DeadlineRiskCard({ metrics }: DeadlineRiskCardProps) { + const riskAnalysis = DeadlineAnalyticsService.calculateRiskMetrics(metrics); + + const getRiskIcon = (level: string) => { + switch (level) { + case 'critical': return '🔴'; + case 'high': return '🟠'; + case 'medium': return '🟡'; + case 'low': return '🟢'; + default: return '⚪'; + } + }; + + const getRiskColor = (level: string) => { + switch (level) { + case 'critical': return 'text-red-600 dark:text-red-400'; + case 'high': return 'text-orange-600 dark:text-orange-400'; + case 'medium': return 'text-yellow-600 dark:text-yellow-400'; + case 'low': return 'text-green-600 dark:text-green-400'; + default: return 'text-gray-600 dark:text-gray-400'; + } + }; + + const getRiskBgColor = (level: string) => { + switch (level) { + case 'critical': return 'bg-red-50/30 border-red-200/50 dark:bg-red-950/20 dark:border-red-800/30'; + case 'high': return 'bg-orange-50/30 border-orange-200/50 dark:bg-orange-950/20 dark:border-orange-800/30'; + case 'medium': return 'bg-yellow-50/30 border-yellow-200/50 dark:bg-yellow-950/20 dark:border-yellow-800/30'; + case 'low': return 'bg-green-50/30 border-green-200/50 dark:bg-green-950/20 dark:border-green-800/30'; + default: return 'bg-gray-50/30 border-gray-200/50 dark:bg-gray-950/20 dark:border-gray-800/30'; + } + }; + + return ( + +
+
+ {getRiskIcon(riskAnalysis.riskLevel)} +

Niveau de Risque

+
+
+ {riskAnalysis.riskScore} +
+
+ +
+ {/* Barre de risque */} +
+
+
+ + {/* Détails des risques */} +
+
+ En retard: + {metrics.summary.overdueCount} +
+
+ Critique: + {metrics.summary.criticalCount} +
+
+ + {/* Recommandation */} +
+

+ {riskAnalysis.recommendation} +

+
+
+ + ); +} diff --git a/src/components/deadline/DeadlineSummaryCard.tsx b/src/components/deadline/DeadlineSummaryCard.tsx new file mode 100644 index 0000000..494aee1 --- /dev/null +++ b/src/components/deadline/DeadlineSummaryCard.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { DeadlineMetrics } from '@/services/analytics/deadline-analytics'; +import { Card } from '@/components/ui/Card'; + +interface DeadlineSummaryCardProps { + metrics: DeadlineMetrics; +} + +export function DeadlineSummaryCard({ metrics }: DeadlineSummaryCardProps) { + const { summary } = metrics; + + const summaryItems = [ + { + label: 'En retard', + count: summary.overdueCount, + icon: '⏰', + color: 'text-red-600 dark:text-red-400', + bgColor: 'bg-red-100/50 dark:bg-red-900/30' + }, + { + label: 'Critique (0-2j)', + count: summary.criticalCount, + icon: '🚨', + color: 'text-orange-600 dark:text-orange-400', + bgColor: 'bg-orange-100/50 dark:bg-orange-900/30' + }, + { + label: 'Attention (3-7j)', + count: summary.warningCount, + icon: '⚠️', + color: 'text-yellow-600 dark:text-yellow-400', + bgColor: 'bg-yellow-100/50 dark:bg-yellow-900/30' + }, + { + label: 'À venir (8-14j)', + count: summary.upcomingCount, + icon: '📅', + color: 'text-blue-600 dark:text-blue-400', + bgColor: 'bg-blue-100/50 dark:bg-blue-900/30' + } + ]; + + return ( + +
+

Répartition des Échéances

+
+ {summary.totalWithDeadlines} total +
+
+ +
+ {summaryItems.map((item, index) => ( +
+
+
+ {item.icon} +
+ {item.label} +
+
+ {item.count} +
+
+ ))} + + {/* Indicateur de performance */} +
+
+ Tâches sous contrôle: + + {summary.totalWithDeadlines - summary.overdueCount - summary.criticalCount}/{summary.totalWithDeadlines} + +
+
+
0 + ? Math.round(((summary.totalWithDeadlines - summary.overdueCount - summary.criticalCount) / summary.totalWithDeadlines) * 100) + : 100 + }%` + }} + /> +
+
+
+ + ); +} diff --git a/src/components/deadline/index.ts b/src/components/deadline/index.ts new file mode 100644 index 0000000..4f93831 --- /dev/null +++ b/src/components/deadline/index.ts @@ -0,0 +1,4 @@ +export { DeadlineOverview } from './DeadlineOverview'; +export { DeadlineRiskCard } from './DeadlineRiskCard'; +export { CriticalDeadlinesCard } from './CriticalDeadlinesCard'; +export { DeadlineSummaryCard } from './DeadlineSummaryCard'; diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts index a2e373b..ff758c4 100644 --- a/src/lib/date-utils.ts +++ b/src/lib/date-utils.ts @@ -3,7 +3,7 @@ * Regroupe toutes les fonctions de formatage, manipulation et validation de dates */ -import { format, startOfDay, endOfDay, isValid } from 'date-fns'; +import { format, startOfDay, endOfDay, isValid, formatDistanceToNow as formatDistanceToNowFns } from 'date-fns'; import { fr } from 'date-fns/locale'; // Re-export des utilitaires workday existants @@ -244,3 +244,17 @@ export function generateDateTitle(date: Date, emoji: string = '📅'): string { return `${emoji} ${formatDateShort(date)}`; } + +/** + * Formate la distance depuis maintenant en français + */ +export function formatDistanceToNow(date: Date, options?: { addSuffix?: boolean }): string { + if (!isValid(date)) { + throw new Error('Date invalide fournie à formatDistanceToNow'); + } + + return formatDistanceToNowFns(date, { + locale: fr, + addSuffix: options?.addSuffix ?? true + }); +} diff --git a/src/services/analytics/analytics.ts b/src/services/analytics/analytics.ts index 1e8391a..64090ed 100644 --- a/src/services/analytics/analytics.ts +++ b/src/services/analytics/analytics.ts @@ -13,6 +13,8 @@ export interface ProductivityMetrics { week: string; completed: number; average: number; + weekNumber?: number; + weekStart?: Date; }>; priorityDistribution: Array<{ priority: string; @@ -137,35 +139,41 @@ export class AnalyticsService { * 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 weeklyData: Array<{ week: string; completed: number; average: number; weekNumber: number; weekStart: Date }> = []; const completedTasks = tasks.filter(task => task.completedAt); - // Grouper par semaine - const weekGroups = new Map(); + // Grouper par semaine en utilisant le numéro de semaine comme clé + 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); + const weekNumber = this.getWeekNumber(weekStart); + + if (!weekGroups.has(weekNumber)) { + weekGroups.set(weekNumber, { count: 0, weekStart }); + } + weekGroups.get(weekNumber)!.count++; } }); // Calculer la moyenne mobile - const values = Array.from(weekGroups.values()); + const values = Array.from(weekGroups.values()).map(w => w.count); 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); + weekGroups.forEach((data, weekNumber) => { weeklyData.push({ - week: `Sem. ${this.getWeekNumber(weekDate)}`, - completed: count, - average: Math.round(average * 10) / 10 + week: `Sem. ${weekNumber}`, + completed: data.count, + average: Math.round(average * 10) / 10, + weekNumber, + weekStart: data.weekStart }); }); - return weeklyData.sort((a, b) => a.week.localeCompare(b.week)); + // Trier par numéro de semaine (pas alphabétiquement) + return weeklyData.sort((a, b) => a.weekNumber - b.weekNumber); } /** diff --git a/src/services/analytics/deadline-analytics.ts b/src/services/analytics/deadline-analytics.ts new file mode 100644 index 0000000..264304e --- /dev/null +++ b/src/services/analytics/deadline-analytics.ts @@ -0,0 +1,206 @@ +import { TaskStatus } from '@/lib/types'; +import { prisma } from '@/services/core/database'; +import { getToday } from '@/lib/date-utils'; + +export interface DeadlineTask { + id: string; + title: string; + status: TaskStatus; + priority: string; + dueDate: Date; + daysRemaining: number; + urgencyLevel: 'overdue' | 'critical' | 'warning' | 'normal'; + source: string; + tags: string[]; + jiraKey?: string; +} + +export interface DeadlineMetrics { + overdue: DeadlineTask[]; + critical: DeadlineTask[]; // 0-2 jours + warning: DeadlineTask[]; // 3-7 jours + upcoming: DeadlineTask[]; // 8-14 jours + summary: { + overdueCount: number; + criticalCount: number; + warningCount: number; + upcomingCount: number; + totalWithDeadlines: number; + }; +} + +export class DeadlineAnalyticsService { + /** + * Analyse les tâches selon leurs échéances + */ + static async getDeadlineMetrics(): Promise { + try { + const now = getToday(); + + // Récupérer toutes les tâches non terminées avec échéance + const dbTasks = await prisma.task.findMany({ + where: { + dueDate: { + not: null + }, + status: { + notIn: ['done', 'cancelled', 'archived'] + } + }, + include: { + taskTags: { + include: { + tag: true + } + } + }, + orderBy: { + dueDate: 'asc' + } + }); + + // Convertir et analyser les tâches + const deadlineTasks: DeadlineTask[] = dbTasks.map(task => { + const dueDate = task.dueDate!; + const daysRemaining = Math.ceil((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); + + let urgencyLevel: DeadlineTask['urgencyLevel']; + if (daysRemaining < 0) { + urgencyLevel = 'overdue'; + } else if (daysRemaining <= 2) { + urgencyLevel = 'critical'; + } else if (daysRemaining <= 7) { + urgencyLevel = 'warning'; + } else { + urgencyLevel = 'normal'; + } + + return { + id: task.id, + title: task.title, + status: task.status as TaskStatus, + priority: task.priority, + dueDate, + daysRemaining, + urgencyLevel, + source: task.source, + tags: task.taskTags.map(tt => tt.tag.name), + jiraKey: task.jiraKey || undefined + }; + }); + + // Filtrer les tâches dans les 2 prochaines semaines + const relevantTasks = deadlineTasks.filter(task => + task.daysRemaining <= 14 || task.urgencyLevel === 'overdue' + ); + + const overdue = relevantTasks.filter(t => t.urgencyLevel === 'overdue'); + const critical = relevantTasks.filter(t => t.urgencyLevel === 'critical'); + const warning = relevantTasks.filter(t => t.urgencyLevel === 'warning'); + const upcoming = relevantTasks.filter(t => t.urgencyLevel === 'normal' && t.daysRemaining <= 14); + + return { + overdue, + critical, + warning, + upcoming, + summary: { + overdueCount: overdue.length, + criticalCount: critical.length, + warningCount: warning.length, + upcomingCount: upcoming.length, + totalWithDeadlines: deadlineTasks.length + } + }; + } catch (error) { + console.error('Erreur lors de l\'analyse des échéances:', error); + throw new Error('Impossible d\'analyser les échéances'); + } + } + + /** + * Retourne les tâches les plus critiques (en retard + échéance dans 48h) + */ + static async getCriticalDeadlines(): Promise { + const metrics = await this.getDeadlineMetrics(); + return [ + ...metrics.overdue, + ...metrics.critical + ].slice(0, 10); // Limite à 10 tâches les plus critiques + } + + /** + * Analyse l'impact des échéances par priorité + */ + static analyzeImpactByPriority(tasks: DeadlineTask[]): Array<{ + priority: string; + count: number; + overdueCount: number; + criticalCount: number; + }> { + const priorityGroups = new Map(); + + tasks.forEach(task => { + const priority = task.priority || 'medium'; + if (!priorityGroups.has(priority)) { + priorityGroups.set(priority, []); + } + priorityGroups.get(priority)!.push(task); + }); + + return Array.from(priorityGroups.entries()).map(([priority, tasks]) => ({ + priority, + count: tasks.length, + overdueCount: tasks.filter(t => t.urgencyLevel === 'overdue').length, + criticalCount: tasks.filter(t => t.urgencyLevel === 'critical').length + })).sort((a, b) => { + // Trier par impact (retard + critique) puis par priorité + const aImpact = a.overdueCount + a.criticalCount; + const bImpact = b.overdueCount + b.criticalCount; + if (aImpact !== bImpact) return bImpact - aImpact; + + const priorityOrder: Record = { urgent: 4, high: 3, medium: 2, low: 1 }; + return (priorityOrder[b.priority] || 2) - (priorityOrder[a.priority] || 2); + }); + } + + /** + * Calcule les métriques de risque + */ + static calculateRiskMetrics(metrics: DeadlineMetrics): { + riskScore: number; // 0-100 + riskLevel: 'low' | 'medium' | 'high' | 'critical'; + recommendation: string; + } { + const { summary } = metrics; + + // Calcul du score de risque basé sur les échéances + let riskScore = 0; + riskScore += summary.overdueCount * 25; // Retard = très grave + riskScore += summary.criticalCount * 15; // Critique = grave + riskScore += summary.warningCount * 5; // Avertissement = attention + riskScore += summary.upcomingCount * 1; // À venir = surveillance + + // Limiter à 100 + riskScore = Math.min(riskScore, 100); + + let riskLevel: 'low' | 'medium' | 'high' | 'critical'; + let recommendation: string; + + if (riskScore >= 75) { + riskLevel = 'critical'; + recommendation = 'Action immédiate requise ! Plusieurs tâches en retard ou critiques.'; + } else if (riskScore >= 50) { + riskLevel = 'high'; + recommendation = 'Attention : échéances critiques approchent, planifier les priorités.'; + } else if (riskScore >= 25) { + riskLevel = 'medium'; + recommendation = 'Surveillance nécessaire, quelques échéances à surveiller.'; + } else { + riskLevel = 'low'; + recommendation = 'Situation stable, échéances sous contrôle.'; + } + + return { riskScore, riskLevel, recommendation }; + } +} diff --git a/src/services/analytics/manager-summary.ts b/src/services/analytics/manager-summary.ts index 91873c8..5f7acf8 100644 --- a/src/services/analytics/manager-summary.ts +++ b/src/services/analytics/manager-summary.ts @@ -1,5 +1,4 @@ import { prisma } from '@/services/core/database'; -import { startOfWeek, endOfWeek } from 'date-fns'; import { getToday } from '@/lib/date-utils'; type TaskType = { @@ -83,11 +82,13 @@ export interface ManagerSummary { export class ManagerSummaryService { /** - * Génère un résumé orienté manager pour la semaine + * Génère un résumé orienté manager pour les 7 derniers jours */ static async getManagerSummary(date: Date = getToday()): Promise { - const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi - const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche + // 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([ @@ -537,22 +538,22 @@ export class ManagerSummaryService { accomplishments: KeyAccomplishment[], challenges: UpcomingChallenge[] ) { - // Points forts de la semaine + // Points forts des 7 derniers jours 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.'; + ? `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 cette semaine.'; + : 'Pas de blockers majeurs rencontrés sur cette période.'; - // Focus semaine prochaine + // Focus 7 prochains jours const topChallenges = challenges.slice(0, 3); const nextWeekFocus = topChallenges.length > 0 - ? `La semaine prochaine sera concentrée sur ${topChallenges.map(c => c.title).join(', ')}.` + ? `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 { diff --git a/src/services/analytics/metrics.ts b/src/services/analytics/metrics.ts index 549c122..e3cc0d8 100644 --- a/src/services/analytics/metrics.ts +++ b/src/services/analytics/metrics.ts @@ -1,5 +1,5 @@ import { prisma } from '@/services/core/database'; -import { startOfWeek, endOfWeek, eachDayOfInterval, format, startOfDay, endOfDay } from 'date-fns'; +import { eachDayOfInterval, format, startOfDay, endOfDay, startOfWeek, endOfWeek } from 'date-fns'; import { fr } from 'date-fns/locale'; import { formatDateForAPI, getDayName, getToday, subtractDays } from '@/lib/date-utils'; @@ -57,11 +57,13 @@ export interface WeeklyMetricsOverview { export class MetricsService { /** - * Récupère les métriques journalières de la semaine + * Récupère les métriques journalières des 7 derniers jours */ static async getWeeklyMetrics(date: Date = getToday()): Promise { - const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi - const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche + // 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) // Générer tous les jours de la semaine const daysOfWeek = eachDayOfInterval({ start: weekStart, end: weekEnd });