Files
towercontrol/src/components/dashboard/ProductivityAnalytics.tsx
2025-10-09 13:40:03 +02:00

207 lines
7.4 KiB
TypeScript

import { useMemo } from 'react';
import { ProductivityMetrics } from '@/services/analytics/analytics';
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
import { TagDistributionMetrics } from '@/services/analytics/tag-analytics';
import { CompletionTrendChart } from '@/components/charts/CompletionTrendChart';
import { VelocityChart } from '@/components/charts/VelocityChart';
import { PriorityDistributionChart } from '@/components/charts/PriorityDistributionChart';
import { WeeklyStatsCard } from '@/components/charts/WeeklyStatsCard';
import { TagDistributionChart } from '@/components/dashboard/TagDistributionChart';
import { Card, MetricCard } from '@/components/ui';
import { DeadlineOverview } from '@/components/deadline/DeadlineOverview';
import { Emoji } from '@/components/ui/Emoji';
interface ProductivityAnalyticsProps {
metrics: ProductivityMetrics;
deadlineMetrics: DeadlineMetrics;
tagMetrics: TagDistributionMetrics;
selectedSources: string[];
hiddenSources?: string[];
}
export function ProductivityAnalytics({
metrics,
deadlineMetrics,
tagMetrics,
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={filteredDeadlineMetrics} />
{/* Titre de section Analytics */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">
<Emoji emoji="📊" /> Analytics & Métriques
</h2>
<div className="text-sm text-[var(--muted-foreground)]">
Derniers 30 jours
</div>
</div>
{/* Performance hebdomadaire */}
<WeeklyStatsCard stats={filteredMetrics.weeklyStats} />
{/* Graphiques principaux */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CompletionTrendChart data={filteredMetrics.completionTrend} />
<VelocityChart data={filteredMetrics.velocityData} />
</div>
{/* Distributions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<PriorityDistributionChart
data={filteredMetrics.priorityDistribution}
/>
{/* Status Flow - Graphique simple en barres horizontales */}
<Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4">Répartition par Statut</h3>
<div className="space-y-3">
{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}
</div>
<div className="flex-1 bg-[var(--border)] rounded-full h-2 relative">
<div
className="bg-gradient-to-r from-blue-500 to-cyan-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${item.percentage}%` }}
></div>
</div>
<div className="w-12 text-sm font-medium text-right">
{item.count}
</div>
<div className="w-10 text-xs text-[var(--muted-foreground)] text-right">
{item.percentage}%
</div>
</div>
))}
</div>
</Card>
</div>
{/* Distribution par Tags */}
<TagDistributionChart metrics={tagMetrics} />
{/* Insights automatiques */}
<Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4">
<Emoji emoji="💡" /> Insights
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard
title="Vélocité Moyenne"
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"
/>
<MetricCard
title="Priorité Principale"
value={
filteredMetrics.priorityDistribution.reduce(
(max, item) => (item.count > max.count ? item : max),
filteredMetrics.priorityDistribution[0]
)?.priority || 'N/A'
}
color="success"
/>
<MetricCard
title="Taux de Completion"
value={`${(() => {
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"
/>
</div>
</Card>
</div>
);
}