- 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.
172 lines
6.6 KiB
TypeScript
172 lines
6.6 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { ProductivityMetrics } from '@/services/analytics/analytics';
|
|
import { DeadlineMetrics } from '@/services/analytics/deadline-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 { Card, MetricCard } from '@/components/ui';
|
|
import { DeadlineOverview } from '@/components/deadline/DeadlineOverview';
|
|
|
|
interface ProductivityAnalyticsProps {
|
|
metrics: ProductivityMetrics;
|
|
deadlineMetrics: DeadlineMetrics;
|
|
selectedSources: string[];
|
|
hiddenSources?: string[];
|
|
}
|
|
|
|
export function ProductivityAnalytics({ metrics, deadlineMetrics, 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">📊 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 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>
|
|
|
|
{/* Insights automatiques */}
|
|
<Card className="p-6">
|
|
<h3 className="text-lg font-semibold mb-4">💡 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>
|
|
);
|
|
}
|