- Changed Card components in various charts and dashboard sections to use the "glass" variant for a consistent UI enhancement. - This update affects CompletionTrendChart, PriorityDistributionChart, VelocityChart, WeeklyStatsCard, DashboardStats, ProductivityAnalytics, RecentTasks, TagDistributionChart, MetricsDistributionCharts, MetricsMainCharts, CriticalDeadlinesCard, DeadlineRiskCard, DeadlineSummaryCard, and StatCard.
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
'use client';
|
|
|
|
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 {
|
|
selectedSources?: string[];
|
|
hiddenSources?: string[];
|
|
}
|
|
|
|
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: filteredStats.total,
|
|
icon: '📋',
|
|
color: 'default' as const
|
|
},
|
|
{
|
|
title: 'À Faire',
|
|
value: filteredStats.todo + filteredStats.backlog,
|
|
icon: '⏳',
|
|
color: 'warning' as const
|
|
},
|
|
{
|
|
title: 'En Cours',
|
|
value: filteredStats.inProgress + filteredStats.freeze,
|
|
icon: '🔄',
|
|
color: 'primary' as const
|
|
},
|
|
{
|
|
title: 'Terminées',
|
|
value: filteredStats.completed + filteredStats.cancelled + filteredStats.archived,
|
|
icon: '✅',
|
|
color: 'success' as const
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
{statCards.map((stat, index) => (
|
|
<StatCard
|
|
key={index}
|
|
title={stat.title}
|
|
value={stat.value}
|
|
icon={stat.icon}
|
|
color={stat.color}
|
|
/>
|
|
))}
|
|
|
|
{/* Cartes de pourcentage */}
|
|
<Card variant="glass" 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
|
|
value={completionRate}
|
|
label="Terminées"
|
|
color="success"
|
|
/>
|
|
|
|
<ProgressBar
|
|
value={inProgressRate}
|
|
label="En Cours"
|
|
color="primary"
|
|
/>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Distribution détaillée par statut */}
|
|
<Card variant="glass" 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 variant="glass" 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">
|
|
{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">
|
|
{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">
|
|
{filteredStats.todo} tâches en attente
|
|
</span>
|
|
</div>
|
|
{totalTasks > 0 && (
|
|
<div className="pt-2 border-t border-[var(--border)]">
|
|
<span className="text-sm font-medium text-[var(--muted-foreground)]">
|
|
Productivité: {completionRate}% de completion
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Distribution par sources */}
|
|
<Card variant="glass" 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>
|
|
);
|
|
}
|