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,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>
);
}