feat: update package dependencies and integrate Recharts

- Changed project name from "towercontrol-temp" to "towercontrol" in package-lock.json and package.json.
- Added Recharts library for data visualization in the dashboard.
- Updated TODO.md to reflect completion of analytics and metrics integration tasks.
- Enhanced RecentTasks component to utilize TaskPriority type for better type safety.
- Minor layout adjustments in RecentTasks for improved UI.
This commit is contained in:
Julien Froidefond
2025-09-18 12:48:06 +02:00
parent d38053b4dc
commit 3c7f5ca2fa
12 changed files with 1253 additions and 13 deletions

View File

@@ -0,0 +1,108 @@
'use client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { Card } from '@/components/ui/Card';
interface CompletionTrendData {
date: string;
completed: number;
created: number;
total: number;
}
interface CompletionTrendChartProps {
data: CompletionTrendData[];
title?: string;
}
export function CompletionTrendChart({ data, title = "Tendance de Completion" }: CompletionTrendChartProps) {
// Formatter pour les dates
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short'
});
};
// Tooltip personnalisé
const CustomTooltip = ({ active, payload, label }: {
active?: boolean;
payload?: Array<{ name: string; value: number; color: string }>;
label?: string
}) => {
if (active && payload && payload.length) {
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
<p className="text-sm font-medium mb-2">{label ? formatDate(label) : ''}</p>
{payload.map((entry, index: number) => (
<p key={index} className="text-sm" style={{ color: entry.color }}>
{entry.name === 'completed' ? 'Terminées' :
entry.name === 'created' ? 'Créées' : 'Total'}: {entry.value}
</p>
))}
</div>
);
}
return null;
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--border)"
opacity={0.3}
/>
<XAxis
dataKey="date"
tickFormatter={formatDate}
stroke="var(--muted-foreground)"
fontSize={12}
tick={{ fill: 'var(--muted-foreground)' }}
/>
<YAxis
stroke="var(--muted-foreground)"
fontSize={12}
tick={{ fill: 'var(--muted-foreground)' }}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="completed"
stroke="#10b981"
strokeWidth={2}
dot={{ fill: '#10b981', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#10b981', strokeWidth: 2 }}
/>
<Line
type="monotone"
dataKey="created"
stroke="#3b82f6"
strokeWidth={2}
strokeDasharray="5 5"
dot={{ fill: '#3b82f6', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#3b82f6', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* Légende */}
<div className="flex items-center justify-center gap-6 mt-4 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-0.5 bg-green-500"></div>
<span className="text-[var(--muted-foreground)]">Tâches terminées</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-0.5 bg-blue-500 border-dashed border-t"></div>
<span className="text-[var(--muted-foreground)]">Tâches créées</span>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,100 @@
'use client';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend, PieLabelRenderProps } from 'recharts';
import { Card } from '@/components/ui/Card';
interface PriorityData {
priority: string;
count: number;
percentage: number;
[key: string]: string | number; // Index signature pour Recharts
}
interface PriorityDistributionChartProps {
data: PriorityData[];
title?: string;
}
// Couleurs pour chaque priorité
const PRIORITY_COLORS = {
'Faible': '#10b981', // green-500
'Moyenne': '#f59e0b', // amber-500
'Élevée': '#8b5cf6', // violet-500
'Urgente': '#ef4444', // red-500
'Non définie': '#6b7280' // gray-500
};
export function PriorityDistributionChart({ data, title = "Distribution des Priorités" }: PriorityDistributionChartProps) {
// Tooltip personnalisé
const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: PriorityData }> }) => {
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.priority}</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)}%` : '';
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
labelLine={false}
label={renderLabel}
outerRadius={80}
fill="#8884d8"
dataKey="count"
nameKey="priority"
>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={PRIORITY_COLORS[entry.priority as keyof typeof PRIORITY_COLORS] || '#6b7280'}
/>
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
<Legend content={<CustomLegend />} />
</PieChart>
</ResponsiveContainer>
</div>
</Card>
);
}

View File

@@ -0,0 +1,94 @@
'use client';
import { Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, ComposedChart } from 'recharts';
import { Card } from '@/components/ui/Card';
interface VelocityData {
week: string;
completed: number;
average: number;
}
interface VelocityChartProps {
data: VelocityData[];
title?: string;
}
export function VelocityChart({ data, title = "Vélocité Hebdomadaire" }: VelocityChartProps) {
// Tooltip personnalisé
const CustomTooltip = ({ active, payload, label }: {
active?: boolean;
payload?: Array<{ dataKey: string; value: number; color: string }>;
label?: string
}) => {
if (active && payload && payload.length) {
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
<p className="text-sm font-medium mb-2">{label}</p>
{payload.map((entry, index: number) => (
<p key={index} className="text-sm" style={{ color: entry.color }}>
{entry.dataKey === 'completed' ? 'Terminées' : 'Moyenne'}: {entry.value}
{entry.dataKey === 'completed' ? ' tâches' : ' tâches/sem'}
</p>
))}
</div>
);
}
return null;
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--border)"
opacity={0.3}
/>
<XAxis
dataKey="week"
stroke="var(--muted-foreground)"
fontSize={12}
tick={{ fill: 'var(--muted-foreground)' }}
/>
<YAxis
stroke="var(--muted-foreground)"
fontSize={12}
tick={{ fill: 'var(--muted-foreground)' }}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey="completed"
fill="#3b82f6"
radius={[4, 4, 0, 0]}
opacity={0.8}
/>
<Line
type="monotone"
dataKey="average"
stroke="#f59e0b"
strokeWidth={3}
dot={{ fill: '#f59e0b', strokeWidth: 2, r: 5 }}
activeDot={{ r: 7, stroke: '#f59e0b', strokeWidth: 2 }}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
{/* Légende */}
<div className="flex items-center justify-center gap-6 mt-4 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-500 rounded-sm opacity-80"></div>
<span className="text-[var(--muted-foreground)]">Tâches terminées</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-0.5 bg-amber-500"></div>
<span className="text-[var(--muted-foreground)]">Moyenne mobile</span>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,78 @@
'use client';
import { Card } from '@/components/ui/Card';
interface WeeklyStats {
thisWeek: number;
lastWeek: number;
change: number;
changePercent: number;
}
interface WeeklyStatsCardProps {
stats: WeeklyStats;
title?: string;
}
export function WeeklyStatsCard({ stats, title = "Performance Hebdomadaire" }: WeeklyStatsCardProps) {
const isPositive = stats.change >= 0;
const changeColor = isPositive ? 'text-[var(--success)]' : 'text-[var(--destructive)]';
const changeIcon = isPositive ? '↗️' : '↘️';
const changeBg = isPositive
? 'bg-[var(--success)]/10 border border-[var(--success)]/20'
: 'bg-[var(--destructive)]/10 border border-[var(--destructive)]/20';
return (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-6">{title}</h3>
<div className="grid grid-cols-2 gap-6">
{/* Cette semaine */}
<div className="text-center">
<div className="text-3xl font-bold text-[var(--primary)] mb-2">
{stats.thisWeek}
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Cette semaine
</div>
</div>
{/* Semaine dernière */}
<div className="text-center">
<div className="text-3xl font-bold text-[var(--muted-foreground)] mb-2">
{stats.lastWeek}
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Semaine dernière
</div>
</div>
</div>
{/* Changement */}
<div className="mt-6 pt-4 border-t border-[var(--border)]">
<div className={`flex items-center justify-center gap-2 p-3 rounded-lg ${changeBg}`}>
<span className="text-lg">{changeIcon}</span>
<div className="text-center">
<div className={`font-bold ${changeColor}`}>
{isPositive ? '+' : ''}{stats.change} tâches
</div>
<div className={`text-sm ${changeColor}`}>
{isPositive ? '+' : ''}{stats.changePercent}% vs semaine dernière
</div>
</div>
</div>
</div>
{/* Insight */}
<div className="mt-4 text-center">
<p className="text-xs text-[var(--muted-foreground)]">
{stats.changePercent > 20 ? 'Excellente progression ! 🚀' :
stats.changePercent > 0 ? 'Bonne progression 👍' :
stats.changePercent === 0 ? 'Performance stable 📊' :
stats.changePercent > -20 ? 'Légère baisse, restez motivé 💪' :
'Focus sur la productivité cette semaine 🎯'}
</p>
</div>
</Card>
);
}