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.
This commit is contained in:
Julien Froidefond
2025-10-02 13:15:10 +02:00
parent 2e3e8bb222
commit 46c1c5e9a1
7 changed files with 600 additions and 39 deletions

View File

@@ -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<string[]>([]);
const [hiddenSources, setHiddenSources] = useState<string[]>([]);
// Handler pour la création de tâche
const handleCreateTask = async (data: CreateTaskData) => {
@@ -59,8 +63,18 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: {
{/* Section de bienvenue */}
<WelcomeSection />
{/* Filtre d'intégrations */}
<div className="flex justify-end mb-6">
<IntegrationFilter
selectedSources={selectedSources}
onSourcesChange={setSelectedSources}
hiddenSources={hiddenSources}
onHiddenSourcesChange={setHiddenSources}
/>
</div>
{/* Statistiques */}
<DashboardStats stats={stats} />
<DashboardStats selectedSources={selectedSources} hiddenSources={hiddenSources} />
{/* Actions rapides */}
<QuickActions onCreateTask={handleCreateTask} />
@@ -69,10 +83,12 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: {
<ProductivityAnalytics
metrics={productivityMetrics}
deadlineMetrics={deadlineMetrics}
selectedSources={selectedSources}
hiddenSources={hiddenSources}
/>
{/* Tâches récentes */}
<RecentTasks tasks={tasks} />
<RecentTasks tasks={tasks} selectedSources={selectedSources} hiddenSources={hiddenSources} />
</main>
</div>
);

View File

@@ -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 (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
<p className="text-sm font-medium mb-1">{data.status}</p>
<p className="text-sm text-[var(--muted-foreground)]">
{data.count} tâches ({data.percentage}%)
</p>
</div>
);
}
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 (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
<p className="text-sm font-medium mb-1">{data.source}</p>
<p className="text-sm text-[var(--muted-foreground)]">
{data.count} tâches ({data.percentage}%)
</p>
</div>
);
}
return null;
};
// Légende personnalisée
const CustomLegend = ({ payload }: { payload?: Array<{ value: string; color: string }> }) => {
return (
<div className="flex flex-wrap justify-center gap-4 mt-4">
{payload?.map((entry, index: number) => (
<div key={index} className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
></div>
<span className="text-sm text-[var(--muted-foreground)]">
{entry.value}
</span>
</div>
))}
</div>
);
};
// 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 */}
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-2 lg:col-span-2">
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-1 lg:col-span-1">
<h3 className="text-lg font-semibold mb-4">Taux de Completion</h3>
<div className="space-y-4">
<ProgressBar
@@ -71,26 +275,59 @@ export function DashboardStats({ stats }: DashboardStatsProps) {
</div>
</Card>
{/* Distribution détaillée par statut */}
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-1 lg:col-span-1">
<h3 className="text-lg font-semibold mb-4">Distribution par Statut</h3>
{/* Graphique en camembert avec Recharts */}
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={statusChartData}
cx="50%"
cy="50%"
labelLine={false}
label={renderLabel}
outerRadius={80}
fill="#8884d8"
dataKey="count"
nameKey="status"
>
{statusChartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
/>
))}
</Pie>
<Tooltip content={<StatusTooltip />} />
<Legend content={<CustomLegend />} />
</PieChart>
</ResponsiveContainer>
</div>
</Card>
{/* Insights rapides */}
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-2 lg:col-span-2">
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-1 lg:col-span-1">
<h3 className="text-lg font-semibold mb-4">Aperçu Rapide</h3>
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('completed').dotColor}`}></span>
<span className="text-sm">
{stats.completed} tâches terminées sur {totalTasks}
{filteredStats.completed} tâches terminées sur {totalTasks}
</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('inProgress').dotColor}`}></span>
<span className="text-sm">
{stats.inProgress} tâches en cours de traitement
{filteredStats.inProgress} tâches en cours de traitement
</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('todo').dotColor}`}></span>
<span className="text-sm">
{stats.todo} tâches en attente
{filteredStats.todo} tâches en attente
</span>
</div>
{totalTasks > 0 && (
@@ -102,6 +339,39 @@ export function DashboardStats({ stats }: DashboardStatsProps) {
)}
</div>
</Card>
{/* Distribution par sources */}
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-1 lg:col-span-1">
<h3 className="text-lg font-semibold mb-4">Distribution par Sources</h3>
{/* Graphique en camembert avec Recharts */}
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={sourceChartData}
cx="50%"
cy="50%"
labelLine={false}
label={renderLabel}
outerRadius={80}
fill="#8884d8"
dataKey="count"
nameKey="source"
>
{sourceChartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
/>
))}
</Pie>
<Tooltip content={<SourceTooltip />} />
<Legend content={<CustomLegend />} />
</PieChart>
</ResponsiveContainer>
</div>
</Card>
</div>
);
}

View File

@@ -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 = (
<div className="space-y-3">
{sources.map((source) => {
const isSelected = selectedSources.includes(source.id);
const isHidden = hiddenSources.includes(source.id);
return (
<div key={source.id} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-medium">
<span>{source.icon}</span>
<span>{source.label}</span>
</div>
<div className="flex items-center gap-1">
{/* Bouton Afficher */}
<button
onClick={(e) => {
e.stopPropagation();
handleModeChange(source.id, 'show');
}}
className={`px-2 py-1 text-xs rounded transition-colors ${
isSelected
? 'bg-[var(--primary)] text-[var(--primary-foreground)]'
: 'bg-[var(--muted)] text-[var(--muted-foreground)] hover:bg-[var(--primary)]/20'
}`}
title="Afficher seulement cette source"
>
</button>
{/* Bouton Masquer */}
<button
onClick={(e) => {
e.stopPropagation();
handleModeChange(source.id, 'hide');
}}
className={`px-2 py-1 text-xs rounded transition-colors ${
isHidden
? 'bg-[var(--destructive)] text-white'
: 'bg-[var(--muted)] text-[var(--muted-foreground)] hover:bg-[var(--destructive)]/20'
}`}
title="Masquer cette source"
>
</button>
</div>
</div>
</div>
);
})}
{/* Option pour réinitialiser tous les filtres */}
<div className="border-t border-[var(--border)] pt-2 mt-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
onHiddenSourcesChange([]);
onSourcesChange([]);
}}
className="w-full justify-start font-mono"
title="Réinitialiser tous les filtres de source"
>
<span>🔄</span>
<span className="flex-1">Réinitialiser tout</span>
</Button>
</div>
</div>
);
return (
<Dropdown
open={isOpen}
onOpenChange={setIsOpen}
trigger={`🔗 ${getMainButtonText()}`}
content={dropdownContent}
/>
);
}

View File

@@ -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 (
<div className="space-y-8">
{/* Section Échéances Critiques */}
<DeadlineOverview metrics={deadlineMetrics} />
<DeadlineOverview metrics={filteredDeadlineMetrics} />
{/* Titre de section Analytics */}
<div className="flex items-center justify-between">
@@ -28,23 +94,23 @@ export function ProductivityAnalytics({ metrics, deadlineMetrics }: Productivity
</div>
{/* Performance hebdomadaire */}
<WeeklyStatsCard stats={metrics.weeklyStats} />
<WeeklyStatsCard stats={filteredMetrics.weeklyStats} />
{/* Graphiques principaux */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CompletionTrendChart data={metrics.completionTrend} />
<VelocityChart data={metrics.velocityData} />
<CompletionTrendChart data={filteredMetrics.completionTrend} />
<VelocityChart data={filteredMetrics.velocityData} />
</div>
{/* Distributions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<PriorityDistributionChart data={metrics.priorityDistribution} />
<PriorityDistributionChart data={filteredMetrics.priorityDistribution} />
{/* Status Flow - Graphique simple en barres horizontales */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Répartition par Statut</h3>
<div className="space-y-3">
{metrics.statusFlow.map((item, index) => (
{filteredMetrics.statusFlow.map((item, index) => (
<div key={index} className="flex items-center gap-3">
<div className="w-20 text-sm text-[var(--muted-foreground)] text-right">
{item.status}
@@ -73,8 +139,8 @@ export function ProductivityAnalytics({ metrics, deadlineMetrics }: Productivity
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard
title="Vélocité Moyenne"
value={`${metrics.velocityData.length > 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
<MetricCard
title="Priorité Principale"
value={metrics.priorityDistribution.reduce((max, item) =>
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
<MetricCard
title="Taux de Completion"
value={`${(() => {
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"

View File

@@ -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);

View File

@@ -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<ProductivityMetrics> {
static async getProductivityMetrics(timeRange?: TimeRange, sources?: string[]): Promise<ProductivityMetrics> {
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),

View File

@@ -33,7 +33,7 @@ export class DeadlineAnalyticsService {
/**
* Analyse les tâches selon leurs échéances
*/
static async getDeadlineMetrics(): Promise<DeadlineMetrics> {
static async getDeadlineMetrics(sources?: string[]): Promise<DeadlineMetrics> {
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<DeadlineTask[]> {
const metrics = await this.getDeadlineMetrics();
static async getCriticalDeadlines(sources?: string[]): Promise<DeadlineTask[]> {
const metrics = await this.getDeadlineMetrics(sources);
return [
...metrics.overdue,
...metrics.critical