207 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|