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:
@@ -8,6 +8,7 @@ import { CreateTaskData } from '@/clients/tasks-client';
|
||||
import { DashboardStats } from '@/components/dashboard/DashboardStats';
|
||||
import { QuickActions } from '@/components/dashboard/QuickActions';
|
||||
import { RecentTasks } from '@/components/dashboard/RecentTasks';
|
||||
import { ProductivityAnalytics } from '@/components/dashboard/ProductivityAnalytics';
|
||||
|
||||
interface HomePageClientProps {
|
||||
initialTasks: Task[];
|
||||
@@ -41,6 +42,9 @@ function HomePageContent() {
|
||||
{/* Actions rapides */}
|
||||
<QuickActions onCreateTask={handleCreateTask} />
|
||||
|
||||
{/* Analytics et métriques */}
|
||||
<ProductivityAnalytics />
|
||||
|
||||
{/* Tâches récentes */}
|
||||
<RecentTasks tasks={tasks} />
|
||||
</main>
|
||||
|
||||
108
components/charts/CompletionTrendChart.tsx
Normal file
108
components/charts/CompletionTrendChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
100
components/charts/PriorityDistributionChart.tsx
Normal file
100
components/charts/PriorityDistributionChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
94
components/charts/VelocityChart.tsx
Normal file
94
components/charts/VelocityChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
78
components/charts/WeeklyStatsCard.tsx
Normal file
78
components/charts/WeeklyStatsCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
162
components/dashboard/ProductivityAnalytics.tsx
Normal file
162
components/dashboard/ProductivityAnalytics.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { TagDisplay } from '@/components/ui/TagDisplay';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { useTasksContext } from '@/contexts/TasksContext';
|
||||
import { getPriorityConfig, getPriorityColorHex } from '@/lib/status-config';
|
||||
import { TaskPriority } from '@/lib/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface RecentTasksProps {
|
||||
@@ -40,7 +41,7 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
|
||||
|
||||
const getPriorityStyle = (priority: string) => {
|
||||
try {
|
||||
const config = getPriorityConfig(priority as any);
|
||||
const config = getPriorityConfig(priority as TaskPriority);
|
||||
const hexColor = getPriorityColorHex(config.color);
|
||||
return { color: hexColor };
|
||||
} catch {
|
||||
@@ -49,7 +50,7 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<Card className="p-6 mt-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">Tâches Récentes</h3>
|
||||
<Link href="/kanban">
|
||||
@@ -103,7 +104,7 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
|
||||
>
|
||||
{(() => {
|
||||
try {
|
||||
return getPriorityConfig(task.priority as any).label;
|
||||
return getPriorityConfig(task.priority as TaskPriority).label;
|
||||
} catch {
|
||||
return task.priority;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user