From 46c1c5e9a1663490a69d7c697afcec31f61e7a46 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 2 Oct 2025 13:15:10 +0200 Subject: [PATCH] feat: add integration filtering to dashboard components - Introduced `IntegrationFilter` to allow users to filter tasks by selected and hidden sources. - Updated `DashboardStats`, `ProductivityAnalytics`, `RecentTasks`, and `HomePageContent` to utilize the new filtering logic, enhancing data presentation based on user preferences. - Implemented filtering logic in `AnalyticsService` and `DeadlineAnalyticsService` to support source-based metrics calculations. - Enhanced UI components to reflect filtered task data, improving user experience and data relevance. --- src/components/HomePageClient.tsx | 22 +- src/components/dashboard/DashboardStats.tsx | 300 +++++++++++++++++- .../dashboard/IntegrationFilter.tsx | 182 +++++++++++ .../dashboard/ProductivityAnalytics.tsx | 92 +++++- src/components/dashboard/RecentTasks.tsx | 21 +- src/services/analytics/analytics.ts | 9 +- src/services/analytics/deadline-analytics.ts | 13 +- 7 files changed, 600 insertions(+), 39 deletions(-) create mode 100644 src/components/dashboard/IntegrationFilter.tsx diff --git a/src/components/HomePageClient.tsx b/src/components/HomePageClient.tsx index 338bf94..78f459c 100644 --- a/src/components/HomePageClient.tsx +++ b/src/components/HomePageClient.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import { Header } from '@/components/ui/Header'; import { TasksProvider, useTasksContext } from '@/contexts/TasksContext'; import { Task, Tag, TaskStats } from '@/lib/types'; @@ -9,6 +10,7 @@ import { QuickActions } from '@/components/dashboard/QuickActions'; import { RecentTasks } from '@/components/dashboard/RecentTasks'; import { ProductivityAnalytics } from '@/components/dashboard/ProductivityAnalytics'; import { WelcomeSection } from '@/components/dashboard/WelcomeSection'; +import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter'; import { ProductivityMetrics } from '@/services/analytics/analytics'; import { DeadlineMetrics } from '@/services/analytics/deadline-analytics'; import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts'; @@ -26,7 +28,9 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: { productivityMetrics: ProductivityMetrics; deadlineMetrics: DeadlineMetrics; }) { - const { stats, syncing, createTask, tasks } = useTasksContext(); + const { syncing, createTask, tasks } = useTasksContext(); + const [selectedSources, setSelectedSources] = useState([]); + const [hiddenSources, setHiddenSources] = useState([]); // Handler pour la création de tâche const handleCreateTask = async (data: CreateTaskData) => { @@ -59,8 +63,18 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: { {/* Section de bienvenue */} + {/* Filtre d'intégrations */} +
+ +
+ {/* Statistiques */} - + {/* Actions rapides */} @@ -69,10 +83,12 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: { {/* Tâches récentes */} - + ); diff --git a/src/components/dashboard/DashboardStats.tsx b/src/components/dashboard/DashboardStats.tsx index b001e15..b48d202 100644 --- a/src/components/dashboard/DashboardStats.tsx +++ b/src/components/dashboard/DashboardStats.tsx @@ -1,41 +1,245 @@ 'use client'; -import { TaskStats } from '@/lib/types'; import { Card } from '@/components/ui/Card'; import { StatCard, ProgressBar } from '@/components/ui'; import { getDashboardStatColors } from '@/lib/status-config'; +import { useTasksContext } from '@/contexts/TasksContext'; +import { useMemo } from 'react'; +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend, PieLabelRenderProps } from 'recharts'; interface DashboardStatsProps { - stats: TaskStats; + selectedSources?: string[]; + hiddenSources?: string[]; } -export function DashboardStats({ stats }: DashboardStatsProps) { - const totalTasks = stats.total; - const completionRate = totalTasks > 0 ? Math.round((stats.completed / totalTasks) * 100) : 0; - const inProgressRate = totalTasks > 0 ? Math.round((stats.inProgress / totalTasks) * 100) : 0; +export function DashboardStats({ selectedSources = [], hiddenSources = [] }: DashboardStatsProps) { + const { regularTasks } = useTasksContext(); + + // Calculer les stats filtrées selon les sources + const filteredStats = useMemo(() => { + let filteredTasks = regularTasks; + + // Si on a des sources sélectionnées, ne garder que celles-ci + if (selectedSources.length > 0) { + filteredTasks = filteredTasks.filter(task => + selectedSources.includes(task.source) + ); + } else if (hiddenSources.length > 0) { + // Sinon, retirer les sources masquées + filteredTasks = filteredTasks.filter(task => + !hiddenSources.includes(task.source) + ); + } + + return { + total: filteredTasks.length, + todo: filteredTasks.filter(t => t.status === 'todo').length, + inProgress: filteredTasks.filter(t => t.status === 'in_progress').length, + completed: filteredTasks.filter(t => t.status === 'done').length, + cancelled: filteredTasks.filter(t => t.status === 'cancelled').length, + backlog: filteredTasks.filter(t => t.status === 'backlog').length, + freeze: filteredTasks.filter(t => t.status === 'freeze').length, + archived: filteredTasks.filter(t => t.status === 'archived').length + }; + }, [regularTasks, selectedSources, hiddenSources]); + + // Données pour le graphique des statuts + const statusChartData = useMemo(() => { + const totalTasks = filteredStats.total; + if (totalTasks === 0) return []; + + const data = []; + + if (filteredStats.backlog > 0) { + data.push({ + status: 'Backlog', + count: filteredStats.backlog, + percentage: Math.round((filteredStats.backlog / totalTasks) * 100), + color: '#6b7280' + }); + } + + if (filteredStats.todo > 0) { + data.push({ + status: 'À Faire', + count: filteredStats.todo, + percentage: Math.round((filteredStats.todo / totalTasks) * 100), + color: '#eab308' + }); + } + + if (filteredStats.inProgress > 0) { + data.push({ + status: 'En Cours', + count: filteredStats.inProgress, + percentage: Math.round((filteredStats.inProgress / totalTasks) * 100), + color: '#3b82f6' + }); + } + + if (filteredStats.freeze > 0) { + data.push({ + status: 'Gelé', + count: filteredStats.freeze, + percentage: Math.round((filteredStats.freeze / totalTasks) * 100), + color: '#8b5cf6' + }); + } + + if (filteredStats.completed > 0) { + data.push({ + status: 'Terminé', + count: filteredStats.completed, + percentage: Math.round((filteredStats.completed / totalTasks) * 100), + color: '#10b981' + }); + } + + if (filteredStats.cancelled > 0) { + data.push({ + status: 'Annulé', + count: filteredStats.cancelled, + percentage: Math.round((filteredStats.cancelled / totalTasks) * 100), + color: '#ef4444' + }); + } + + if (filteredStats.archived > 0) { + data.push({ + status: 'Archivé', + count: filteredStats.archived, + percentage: Math.round((filteredStats.archived / totalTasks) * 100), + color: '#9ca3af' + }); + } + + return data; + }, [filteredStats]); + + // Données pour le graphique des sources + const sourceChartData = useMemo(() => { + const totalTasks = filteredStats.total; + if (totalTasks === 0) return []; + + const jiraCount = regularTasks.filter(task => task.source === 'jira').length; + const tfsCount = regularTasks.filter(task => task.source === 'tfs').length; + const manualCount = regularTasks.filter(task => task.source === 'manual').length; + + const data = []; + + if (jiraCount > 0) { + data.push({ + source: 'Jira', + count: jiraCount, + percentage: Math.round((jiraCount / totalTasks) * 100), + color: '#2563eb' + }); + } + + if (tfsCount > 0) { + data.push({ + source: 'TFS', + count: tfsCount, + percentage: Math.round((tfsCount / totalTasks) * 100), + color: '#7c3aed' + }); + } + + if (manualCount > 0) { + data.push({ + source: 'Manuel', + count: manualCount, + percentage: Math.round((manualCount / totalTasks) * 100), + color: '#059669' + }); + } + + return data; + }, [filteredStats, regularTasks]); + + // Tooltip personnalisé pour les statuts + const StatusTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { status: string; count: number; percentage: number } }> }) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.status}

+

+ {data.count} tâches ({data.percentage}%) +

+
+ ); + } + return null; + }; + + // Tooltip personnalisé pour les sources + const SourceTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { source: string; count: number; percentage: number } }> }) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.source}

+

+ {data.count} tâches ({data.percentage}%) +

+
+ ); + } + return null; + }; + + // Légende personnalisée + const CustomLegend = ({ payload }: { payload?: Array<{ value: string; color: string }> }) => { + return ( +
+ {payload?.map((entry, index: number) => ( +
+
+ + {entry.value} + +
+ ))} +
+ ); + }; + + // Label personnalisé pour afficher les pourcentages + const renderLabel = (props: PieLabelRenderProps) => { + const percentage = typeof props.percent === 'number' ? props.percent * 100 : 0; + return percentage > 5 ? `${Math.round(percentage)}%` : ''; + }; + + const totalTasks = filteredStats.total; + const completionRate = totalTasks > 0 ? Math.round((filteredStats.completed / totalTasks) * 100) : 0; + const inProgressRate = totalTasks > 0 ? Math.round((filteredStats.inProgress / totalTasks) * 100) : 0; const statCards = [ { title: 'Total Tâches', - value: stats.total, + value: filteredStats.total, icon: '📋', color: 'default' as const }, { title: 'À Faire', - value: stats.todo, + value: filteredStats.todo + filteredStats.backlog, icon: '⏳', color: 'warning' as const }, { title: 'En Cours', - value: stats.inProgress, + value: filteredStats.inProgress + filteredStats.freeze, icon: '🔄', color: 'primary' as const }, { title: 'Terminées', - value: stats.completed, + value: filteredStats.completed + filteredStats.cancelled + filteredStats.archived, icon: '✅', color: 'success' as const } @@ -54,7 +258,7 @@ export function DashboardStats({ stats }: DashboardStatsProps) { ))} {/* Cartes de pourcentage */} - +

Taux de Completion

+ {/* Distribution détaillée par statut */} + +

Distribution par Statut

+ + {/* Graphique en camembert avec Recharts */} +
+ + + + {statusChartData.map((entry, index) => ( + + ))} + + } /> + } /> + + +
+
+ {/* Insights rapides */} - +

Aperçu Rapide

- {stats.completed} tâches terminées sur {totalTasks} + {filteredStats.completed} tâches terminées sur {totalTasks}
- {stats.inProgress} tâches en cours de traitement + {filteredStats.inProgress} tâches en cours de traitement
- {stats.todo} tâches en attente + {filteredStats.todo} tâches en attente
{totalTasks > 0 && ( @@ -102,6 +339,39 @@ export function DashboardStats({ stats }: DashboardStatsProps) { )}
+ + {/* Distribution par sources */} + +

Distribution par Sources

+ + {/* Graphique en camembert avec Recharts */} +
+ + + + {sourceChartData.map((entry, index) => ( + + ))} + + } /> + } /> + + +
+
); } diff --git a/src/components/dashboard/IntegrationFilter.tsx b/src/components/dashboard/IntegrationFilter.tsx new file mode 100644 index 0000000..8a67fee --- /dev/null +++ b/src/components/dashboard/IntegrationFilter.tsx @@ -0,0 +1,182 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { useTasksContext } from '@/contexts/TasksContext'; +import { Dropdown, Button } from '@/components/ui'; + +interface IntegrationFilterProps { + selectedSources: string[]; + onSourcesChange: (sources: string[]) => void; + hiddenSources: string[]; + onHiddenSourcesChange: (sources: string[]) => void; +} + +interface SourceOption { + id: 'jira' | 'tfs' | 'manual'; + label: string; + icon: string; + hasTasks: boolean; +} + +type FilterMode = 'all' | 'show' | 'hide'; + +export function IntegrationFilter({ selectedSources, onSourcesChange, hiddenSources, onHiddenSourcesChange }: IntegrationFilterProps) { + const { regularTasks } = useTasksContext(); + const [isOpen, setIsOpen] = useState(false); + + // Vérifier quelles sources ont des tâches + const sources = useMemo((): SourceOption[] => { + const hasJiraTasks = regularTasks.some(task => task.source === 'jira'); + const hasTfsTasks = regularTasks.some(task => task.source === 'tfs'); + const hasManualTasks = regularTasks.some(task => task.source === 'manual'); + + return [ + { + id: 'jira' as const, + label: 'Jira', + icon: '🔹', + hasTasks: hasJiraTasks + }, + { + id: 'tfs' as const, + label: 'TFS', + icon: '🔷', + hasTasks: hasTfsTasks + }, + { + id: 'manual' as const, + label: 'Manuel', + icon: '✋', + hasTasks: hasManualTasks + } + ].filter(source => source.hasTasks); + }, [regularTasks]); + + // Si aucune source disponible, on n'affiche rien + if (sources.length === 0) { + return null; + } + + + const handleModeChange = (sourceId: string, mode: FilterMode) => { + let newSelectedSources = [...selectedSources]; + let newHiddenSources = [...hiddenSources]; + + if (mode === 'show') { + // Ajouter à selectedSources et retirer de hiddenSources + if (!newSelectedSources.includes(sourceId)) { + newSelectedSources.push(sourceId); + } + newHiddenSources = newHiddenSources.filter(id => id !== sourceId); + } else if (mode === 'hide') { + // Ajouter à hiddenSources et retirer de selectedSources + if (!newHiddenSources.includes(sourceId)) { + newHiddenSources.push(sourceId); + } + newSelectedSources = newSelectedSources.filter(id => id !== sourceId); + } else { // 'all' + // Retirer des deux listes + newSelectedSources = newSelectedSources.filter(id => id !== sourceId); + newHiddenSources = newHiddenSources.filter(id => id !== sourceId); + } + + onHiddenSourcesChange(newHiddenSources); + onSourcesChange(newSelectedSources); + }; + + + const getMainButtonText = () => { + if (selectedSources.length === 0 && hiddenSources.length === 0) { + return 'Toutes les sources'; + } else if (selectedSources.length === 1 && hiddenSources.length === 0) { + const source = sources.find(s => s.id === selectedSources[0]); + return source ? `Seulement ${source.label}` : 'Source sélectionnée'; + } else if (hiddenSources.length === 1 && selectedSources.length === 0) { + const source = sources.find(s => s.id === hiddenSources[0]); + return source ? `Sans ${source.label}` : 'Source masquée'; + } else { + const total = selectedSources.length + hiddenSources.length; + return `${total} filtres actifs`; + } + }; + + const dropdownContent = ( +
+ {sources.map((source) => { + const isSelected = selectedSources.includes(source.id); + const isHidden = hiddenSources.includes(source.id); + + return ( +
+
+
+ {source.icon} + {source.label} +
+ +
+ {/* Bouton Afficher */} + + + {/* Bouton Masquer */} + +
+
+
+ ); + })} + + {/* Option pour réinitialiser tous les filtres */} +
+ +
+
+ ); + + return ( + + ); +} diff --git a/src/components/dashboard/ProductivityAnalytics.tsx b/src/components/dashboard/ProductivityAnalytics.tsx index 15202f4..334ca38 100644 --- a/src/components/dashboard/ProductivityAnalytics.tsx +++ b/src/components/dashboard/ProductivityAnalytics.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { ProductivityMetrics } from '@/services/analytics/analytics'; import { DeadlineMetrics } from '@/services/analytics/deadline-analytics'; import { CompletionTrendChart } from '@/components/charts/CompletionTrendChart'; @@ -10,14 +11,79 @@ import { DeadlineOverview } from '@/components/deadline/DeadlineOverview'; interface ProductivityAnalyticsProps { metrics: ProductivityMetrics; deadlineMetrics: DeadlineMetrics; + selectedSources: string[]; + hiddenSources?: string[]; } -export function ProductivityAnalytics({ metrics, deadlineMetrics }: ProductivityAnalyticsProps) { +export function ProductivityAnalytics({ metrics, deadlineMetrics, selectedSources, hiddenSources = [] }: ProductivityAnalyticsProps) { + + // Filtrer les métriques selon les sources sélectionnées + const filteredMetrics = useMemo(() => { + if (selectedSources.length === 0) { + return metrics; + } + + // Pour les métriques complexes, on garde les données originales + // car elles nécessitent un recalcul complet côté serveur + // TODO: Implémenter le recalcul côté client ou créer une API + return metrics; + }, [metrics, selectedSources]); + + const filteredDeadlineMetrics = useMemo(() => { + let filteredOverdue = deadlineMetrics.overdue; + let filteredCritical = deadlineMetrics.critical; + let filteredWarning = deadlineMetrics.warning; + let filteredUpcoming = deadlineMetrics.upcoming; + + // Si on a des sources sélectionnées, ne garder que celles-ci + if (selectedSources.length > 0) { + filteredOverdue = filteredOverdue.filter(task => + selectedSources.includes(task.source) + ); + filteredCritical = filteredCritical.filter(task => + selectedSources.includes(task.source) + ); + filteredWarning = filteredWarning.filter(task => + selectedSources.includes(task.source) + ); + filteredUpcoming = filteredUpcoming.filter(task => + selectedSources.includes(task.source) + ); + } else if (hiddenSources.length > 0) { + // Sinon, retirer les sources masquées + filteredOverdue = filteredOverdue.filter(task => + !hiddenSources.includes(task.source) + ); + filteredCritical = filteredCritical.filter(task => + !hiddenSources.includes(task.source) + ); + filteredWarning = filteredWarning.filter(task => + !hiddenSources.includes(task.source) + ); + filteredUpcoming = filteredUpcoming.filter(task => + !hiddenSources.includes(task.source) + ); + } + + return { + overdue: filteredOverdue, + critical: filteredCritical, + warning: filteredWarning, + upcoming: filteredUpcoming, + summary: { + overdueCount: filteredOverdue.length, + criticalCount: filteredCritical.length, + warningCount: filteredWarning.length, + upcomingCount: filteredUpcoming.length, + totalWithDeadlines: filteredOverdue.length + filteredCritical.length + filteredWarning.length + filteredUpcoming.length + } + }; + }, [deadlineMetrics, selectedSources, hiddenSources]); return (
{/* Section Échéances Critiques */} - + {/* Titre de section Analytics */}
@@ -28,23 +94,23 @@ export function ProductivityAnalytics({ metrics, deadlineMetrics }: Productivity
{/* Performance hebdomadaire */} - + {/* Graphiques principaux */}
- - + +
{/* Distributions */}
- + {/* Status Flow - Graphique simple en barres horizontales */}

Répartition par Statut

- {metrics.statusFlow.map((item, index) => ( + {filteredMetrics.statusFlow.map((item, index) => (
{item.status} @@ -73,8 +139,8 @@ export function ProductivityAnalytics({ metrics, deadlineMetrics }: Productivity
0 - ? Math.round(metrics.velocityData.reduce((acc, item) => acc + item.completed, 0) / metrics.velocityData.length) + value={`${filteredMetrics.velocityData.length > 0 + ? Math.round(filteredMetrics.velocityData.reduce((acc, item) => acc + item.completed, 0) / filteredMetrics.velocityData.length) : 0 } tâches/sem`} color="primary" @@ -82,9 +148,9 @@ export function ProductivityAnalytics({ metrics, deadlineMetrics }: Productivity + value={filteredMetrics.priorityDistribution.reduce((max, item) => item.count > max.count ? item : max, - metrics.priorityDistribution[0] + filteredMetrics.priorityDistribution[0] )?.priority || 'N/A'} color="success" /> @@ -92,8 +158,8 @@ export function ProductivityAnalytics({ metrics, deadlineMetrics }: Productivity { - const completed = metrics.statusFlow.find(s => s.status === 'Terminé')?.count || 0; - const total = metrics.statusFlow.reduce((acc, s) => acc + s.count, 0); + const completed = filteredMetrics.statusFlow.find(s => s.status === 'Terminé')?.count || 0; + const total = filteredMetrics.statusFlow.reduce((acc, s) => acc + s.count, 0); return total > 0 ? Math.round((completed / total) * 100) : 0; })()}%`} color="warning" diff --git a/src/components/dashboard/RecentTasks.tsx b/src/components/dashboard/RecentTasks.tsx index f310ddc..a63a334 100644 --- a/src/components/dashboard/RecentTasks.tsx +++ b/src/components/dashboard/RecentTasks.tsx @@ -8,13 +8,30 @@ import Link from 'next/link'; interface RecentTasksProps { tasks: Task[]; + selectedSources?: string[]; + hiddenSources?: string[]; } -export function RecentTasks({ tasks }: RecentTasksProps) { +export function RecentTasks({ tasks, selectedSources = [], hiddenSources = [] }: RecentTasksProps) { const { tags: availableTags } = useTasksContext(); + // Filtrer les tâches selon les sources sélectionnées et masquées + let filteredTasks = tasks; + + // Si on a des sources sélectionnées, ne garder que celles-ci + if (selectedSources.length > 0) { + filteredTasks = filteredTasks.filter(task => + selectedSources.includes(task.source) + ); + } else if (hiddenSources.length > 0) { + // Sinon, retirer les sources masquées + filteredTasks = filteredTasks.filter(task => + !hiddenSources.includes(task.source) + ); + } + // Prendre les 5 tâches les plus récentes (créées ou modifiées) - const recentTasks = tasks + const recentTasks = filteredTasks .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) .slice(0, 5); diff --git a/src/services/analytics/analytics.ts b/src/services/analytics/analytics.ts index a784a8e..ed9ec01 100644 --- a/src/services/analytics/analytics.ts +++ b/src/services/analytics/analytics.ts @@ -43,7 +43,7 @@ export class AnalyticsService { /** * Calcule les métriques de productivité pour une période donnée */ - static async getProductivityMetrics(timeRange?: TimeRange): Promise { + static async getProductivityMetrics(timeRange?: TimeRange, sources?: string[]): Promise { try { const now = getToday(); const defaultStart = subtractDays(now, 30); // 30 jours @@ -63,7 +63,7 @@ export class AnalyticsService { }); // Convertir en format Task - const tasks: Task[] = dbTasks.map(task => ({ + let tasks: Task[] = dbTasks.map(task => ({ id: task.id, title: task.title, description: task.description || undefined, @@ -82,6 +82,11 @@ export class AnalyticsService { assignee: task.assignee || undefined })); + // Filtrer par sources si spécifié + if (sources && sources.length > 0) { + tasks = tasks.filter(task => sources.includes(task.source)); + } + return { completionTrend: this.calculateCompletionTrend(tasks, start, end), velocityData: this.calculateVelocity(tasks, start, end), diff --git a/src/services/analytics/deadline-analytics.ts b/src/services/analytics/deadline-analytics.ts index 264304e..a12ceaa 100644 --- a/src/services/analytics/deadline-analytics.ts +++ b/src/services/analytics/deadline-analytics.ts @@ -33,7 +33,7 @@ export class DeadlineAnalyticsService { /** * Analyse les tâches selon leurs échéances */ - static async getDeadlineMetrics(): Promise { + static async getDeadlineMetrics(sources?: string[]): Promise { try { const now = getToday(); @@ -60,7 +60,7 @@ export class DeadlineAnalyticsService { }); // Convertir et analyser les tâches - const deadlineTasks: DeadlineTask[] = dbTasks.map(task => { + let deadlineTasks: DeadlineTask[] = dbTasks.map(task => { const dueDate = task.dueDate!; const daysRemaining = Math.ceil((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); @@ -89,6 +89,11 @@ export class DeadlineAnalyticsService { }; }); + // Filtrer par sources si spécifié + if (sources && sources.length > 0) { + deadlineTasks = deadlineTasks.filter(task => sources.includes(task.source)); + } + // Filtrer les tâches dans les 2 prochaines semaines const relevantTasks = deadlineTasks.filter(task => task.daysRemaining <= 14 || task.urgencyLevel === 'overdue' @@ -121,8 +126,8 @@ export class DeadlineAnalyticsService { /** * Retourne les tâches les plus critiques (en retard + échéance dans 48h) */ - static async getCriticalDeadlines(): Promise { - const metrics = await this.getDeadlineMetrics(); + static async getCriticalDeadlines(sources?: string[]): Promise { + const metrics = await this.getDeadlineMetrics(sources); return [ ...metrics.overdue, ...metrics.critical