Files
towercontrol/src/components/dashboard/DashboardStats.tsx
Julien Froidefond 0658b8ff93 feat: update Card components to use variant="glass"
- 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.
2025-10-03 17:29:46 +02:00

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