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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user