- Updated `TODO.md` to reflect new testing tasks and final structure expectations. - Simplified TypeScript path mappings in `tsconfig.json` for better clarity. - Revised business logic separation rules in `.cursor/rules` to align with new directory structure. - Deleted unused client components and services to streamline the codebase. - Adjusted import paths in scripts to match the new structure.
163 lines
6.3 KiB
TypeScript
163 lines
6.3 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect, useTransition } from 'react';
|
||
import { ProductivityMetrics } from '@/services/analytics';
|
||
import { getProductivityMetrics } from '@/actions/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 } from '@/components/ui/Card';
|
||
|
||
export function ProductivityAnalytics() {
|
||
const [metrics, setMetrics] = useState<ProductivityMetrics | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [isPending, startTransition] = useTransition();
|
||
|
||
useEffect(() => {
|
||
const loadMetrics = () => {
|
||
startTransition(async () => {
|
||
try {
|
||
setError(null);
|
||
const response = await getProductivityMetrics();
|
||
|
||
if (response.success && response.data) {
|
||
setMetrics(response.data);
|
||
} else {
|
||
setError(response.error || 'Erreur lors du chargement des métriques');
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Erreur lors du chargement des métriques');
|
||
console.error('Erreur analytics:', err);
|
||
}
|
||
});
|
||
};
|
||
|
||
loadMetrics();
|
||
}, []);
|
||
|
||
if (isPending) {
|
||
return (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<Card key={i} className="p-6 animate-pulse">
|
||
<div className="h-4 bg-[var(--border)] rounded mb-4 w-1/3"></div>
|
||
<div className="h-64 bg-[var(--border)] rounded"></div>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<Card className="p-6 mb-8 mt-8">
|
||
<div className="text-center">
|
||
<div className="text-red-500 text-4xl mb-2">⚠️</div>
|
||
<h3 className="text-lg font-semibold mb-2">Erreur de chargement</h3>
|
||
<p className="text-[var(--muted-foreground)] text-sm">{error}</p>
|
||
</div>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (!metrics) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-8">
|
||
{/* Titre de section */}
|
||
<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={metrics.weeklyStats} />
|
||
|
||
{/* Graphiques principaux */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<CompletionTrendChart data={metrics.completionTrend} />
|
||
<VelocityChart data={metrics.velocityData} />
|
||
</div>
|
||
|
||
{/* Distributions */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<PriorityDistributionChart data={metrics.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">
|
||
{metrics.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">
|
||
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--primary)]/50 transition-colors">
|
||
<div className="text-[var(--primary)] font-medium text-sm mb-1">
|
||
Vélocité Moyenne
|
||
</div>
|
||
<div className="text-2xl font-bold text-[var(--foreground)]">
|
||
{metrics.velocityData.length > 0
|
||
? Math.round(metrics.velocityData.reduce((acc, item) => acc + item.completed, 0) / metrics.velocityData.length)
|
||
: 0
|
||
} <span className="text-sm font-normal text-[var(--muted-foreground)]">tâches/sem</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--success)]/50 transition-colors">
|
||
<div className="text-[var(--success)] font-medium text-sm mb-1">
|
||
Priorité Principale
|
||
</div>
|
||
<div className="text-lg font-bold text-[var(--foreground)]">
|
||
{metrics.priorityDistribution.reduce((max, item) =>
|
||
item.count > max.count ? item : max,
|
||
metrics.priorityDistribution[0]
|
||
)?.priority || 'N/A'}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--accent)]/50 transition-colors">
|
||
<div className="text-[var(--accent)] font-medium text-sm mb-1">
|
||
Taux de Completion
|
||
</div>
|
||
<div className="text-2xl font-bold text-[var(--foreground)]">
|
||
{(() => {
|
||
const completed = metrics.statusFlow.find(s => s.status === 'Terminé')?.count || 0;
|
||
const total = metrics.statusFlow.reduce((acc, s) => acc + s.count, 0);
|
||
return total > 0 ? Math.round((completed / total) * 100) : 0;
|
||
})()}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|