chore: refactor project structure and clean up unused components

- 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.
This commit is contained in:
Julien Froidefond
2025-09-21 10:26:35 +02:00
parent 9dc1fafa76
commit 4152b0bdfc
130 changed files with 360 additions and 413 deletions

View File

@@ -0,0 +1,146 @@
'use client';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
interface CategoryData {
count: number;
percentage: number;
color: string;
icon: string;
}
interface CategoryBreakdownProps {
categoryData: { [categoryName: string]: CategoryData };
totalActivities: number;
}
export function CategoryBreakdown({ categoryData, totalActivities }: CategoryBreakdownProps) {
const categories = Object.entries(categoryData)
.filter(([, data]) => data.count > 0)
.sort((a, b) => b[1].count - a[1].count);
if (categories.length === 0) {
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">📊 Répartition par catégorie</h3>
</CardHeader>
<CardContent>
<p className="text-center text-[var(--muted-foreground)]">
Aucune activité à catégoriser
</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">📊 Répartition par catégorie</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Analyse automatique de vos {totalActivities} activités
</p>
</CardHeader>
<CardContent className="space-y-6">
{/* Légende des catégories */}
<div className="flex flex-wrap gap-3 justify-center">
{categories.map(([categoryName, data]) => (
<div
key={categoryName}
className="flex items-center gap-2 bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-2 hover:border-[var(--primary)]/50 transition-colors"
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: data.color }}
/>
<span className="text-sm font-medium text-[var(--foreground)]">
{data.icon} {categoryName}
</span>
<Badge className="bg-[var(--primary)]/10 text-[var(--primary)] text-xs">
{data.count}
</Badge>
</div>
))}
</div>
{/* Barres de progression */}
<div className="space-y-3">
{categories.map(([categoryName, data]) => (
<div key={categoryName} className="space-y-1">
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2">
<span>{data.icon}</span>
<span className="font-medium">{categoryName}</span>
</span>
<span className="text-[var(--muted-foreground)]">
{data.count} ({data.percentage.toFixed(1)}%)
</span>
</div>
<div className="w-full bg-[var(--border)] rounded-full h-2">
<div
className="h-2 rounded-full transition-all duration-500"
style={{
backgroundColor: data.color,
width: `${data.percentage}%`
}}
/>
</div>
</div>
))}
</div>
{/* Insights */}
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)]">
<h4 className="font-medium mb-2">💡 Insights</h4>
<div className="text-sm text-[var(--muted-foreground)] space-y-1">
{categories.length > 0 && (
<>
<p>
🏆 <strong>{categories[0][0]}</strong> est votre activité principale
({categories[0][1].percentage.toFixed(1)}% de votre temps).
</p>
{categories.length > 1 && (
<p>
📈 Vous avez une bonne diversité avec {categories.length} catégories d&apos;activités.
</p>
)}
{/* Suggestions basées sur la répartition */}
{categories.some(([, data]) => data.percentage > 70) && (
<p>
Forte concentration sur une seule catégorie.
Pensez à diversifier vos activités pour un meilleur équilibre.
</p>
)}
{(() => {
const learningCategory = categories.find(([name]) => name === 'Learning');
return learningCategory && learningCategory[1].percentage > 0 && (
<p>
🎓 Excellent ! Vous consacrez du temps à l&apos;apprentissage
({learningCategory[1].percentage.toFixed(1)}%).
</p>
);
})()}
{(() => {
const devCategory = categories.find(([name]) => name === 'Dev');
return devCategory && devCategory[1].percentage > 50 && (
<p>
💻 Focus développement intense. N&apos;oubliez pas les pauses et la collaboration !
</p>
);
})()}
</>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,128 @@
'use client';
import { TaskStats } from '@/lib/types';
import { Card } from '@/components/ui/Card';
import { getDashboardStatColors } from '@/lib/status-config';
interface DashboardStatsProps {
stats: TaskStats;
}
export function DashboardStats({ stats }: DashboardStatsProps) {
const totalTasks = stats.total;
const completionRate = totalTasks > 0 ? Math.round((stats.completed / totalTasks) * 100) : 0;
const inProgressRate = totalTasks > 0 ? Math.round((stats.inProgress / totalTasks) * 100) : 0;
const statCards = [
{
title: 'Total Tâches',
value: stats.total,
icon: '📋',
type: 'total' as const,
...getDashboardStatColors('total')
},
{
title: 'À Faire',
value: stats.todo,
icon: '⏳',
type: 'todo' as const,
...getDashboardStatColors('todo')
},
{
title: 'En Cours',
value: stats.inProgress,
icon: '🔄',
type: 'inProgress' as const,
...getDashboardStatColors('inProgress')
},
{
title: 'Terminées',
value: stats.completed,
icon: '✅',
type: 'completed' as const,
...getDashboardStatColors('completed')
}
];
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{statCards.map((stat, index) => (
<Card key={index} className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--muted-foreground)] mb-1">
{stat.title}
</p>
<p className={`text-3xl font-bold ${stat.textColor}`}>
{stat.value}
</p>
</div>
<div className="text-3xl">
{stat.icon}
</div>
</div>
</Card>
))}
{/* Cartes de pourcentage */}
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-2 lg:col-span-2">
<h3 className="text-lg font-semibold mb-4">Taux de Completion</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Terminées</span>
<span className={`font-bold ${getDashboardStatColors('completed').textColor}`}>{completionRate}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getDashboardStatColors('completed').progressColor}`}
style={{ width: `${completionRate}%` }}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">En Cours</span>
<span className={`font-bold ${getDashboardStatColors('inProgress').textColor}`}>{inProgressRate}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getDashboardStatColors('inProgress').progressColor}`}
style={{ width: `${inProgressRate}%` }}
/>
</div>
</div>
</Card>
{/* Insights rapides */}
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-2 lg:col-span-2">
<h3 className="text-lg font-semibold mb-4">Aperçu Rapide</h3>
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('completed').dotColor}`}></span>
<span className="text-sm">
{stats.completed} tâches terminées sur {totalTasks}
</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('inProgress').dotColor}`}></span>
<span className="text-sm">
{stats.inProgress} tâches en cours de traitement
</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('todo').dotColor}`}></span>
<span className="text-sm">
{stats.todo} tâches en attente
</span>
</div>
{totalTasks > 0 && (
<div className="pt-2 border-t border-[var(--border)]">
<span className="text-sm font-medium text-[var(--muted-foreground)]">
Productivité: {completionRate}% de completion
</span>
</div>
)}
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,193 @@
'use client';
import type { JiraWeeklyMetrics } from '@/services/jira-summary';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { JiraSummaryService } from '@/services/jira-summary';
interface JiraWeeklyMetricsProps {
jiraMetrics: JiraWeeklyMetrics | null;
}
export function JiraWeeklyMetrics({ jiraMetrics }: JiraWeeklyMetricsProps) {
if (!jiraMetrics) {
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🔗 Contexte business Jira</h3>
</CardHeader>
<CardContent>
<p className="text-center text-[var(--muted-foreground)]">
Configuration Jira non disponible
</p>
</CardContent>
</Card>
);
}
if (jiraMetrics.totalJiraTasks === 0) {
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🔗 Contexte business Jira</h3>
</CardHeader>
<CardContent>
<p className="text-center text-[var(--muted-foreground)]">
Aucune tâche Jira cette semaine
</p>
</CardContent>
</Card>
);
}
const completionRate = (jiraMetrics.completedJiraTasks / jiraMetrics.totalJiraTasks) * 100;
const insights = JiraSummaryService.generateBusinessInsights(jiraMetrics);
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🔗 Contexte business Jira</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Impact business et métriques projet
</p>
</CardHeader>
<CardContent className="space-y-6">
{/* Métriques principales */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--primary)]/50 transition-colors text-center">
<div className="text-2xl font-bold text-[var(--primary)]">
{jiraMetrics.totalJiraTasks}
</div>
<div className="text-sm text-[var(--muted-foreground)]">Tickets Jira</div>
</div>
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--success)]/50 transition-colors text-center">
<div className="text-2xl font-bold text-[var(--success)]">
{completionRate.toFixed(0)}%
</div>
<div className="text-sm text-[var(--muted-foreground)]">Taux completion</div>
</div>
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--accent)]/50 transition-colors text-center">
<div className="text-2xl font-bold text-[var(--accent)]">
{jiraMetrics.totalStoryPoints}
</div>
<div className="text-sm text-[var(--muted-foreground)]">Story Points*</div>
</div>
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--warning)]/50 transition-colors text-center">
<div className="text-2xl font-bold text-[var(--warning)]">
{jiraMetrics.projectsContributed.length}
</div>
<div className="text-sm text-[var(--muted-foreground)]">Projet(s)</div>
</div>
</div>
{/* Projets contributés */}
{jiraMetrics.projectsContributed.length > 0 && (
<div>
<h4 className="font-medium mb-2">📂 Projets contributés</h4>
<div className="flex flex-wrap gap-2">
{jiraMetrics.projectsContributed.map(project => (
<Badge key={project} className="bg-[var(--primary)]/10 text-[var(--primary)]">
{project}
</Badge>
))}
</div>
</div>
)}
{/* Types de tickets */}
<div>
<h4 className="font-medium mb-3">🎯 Types de tickets</h4>
<div className="space-y-2">
{Object.entries(jiraMetrics.ticketTypes)
.sort(([,a], [,b]) => b - a)
.map(([type, count]) => {
const percentage = (count / jiraMetrics.totalJiraTasks) * 100;
return (
<div key={type} className="flex items-center justify-between">
<span className="text-sm text-[var(--foreground)]">{type}</span>
<div className="flex items-center gap-2">
<div className="w-20 bg-[var(--border)] rounded-full h-2">
<div
className="h-2 bg-[var(--primary)] rounded-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-sm text-[var(--muted-foreground)] w-8">
{count}
</span>
</div>
</div>
);
})}
</div>
</div>
{/* Liens vers les tickets */}
<div>
<h4 className="font-medium mb-3">🎫 Tickets traités</h4>
<div className="space-y-2 max-h-40 overflow-y-auto">
{jiraMetrics.jiraLinks.map((link) => (
<div
key={link.key}
className="flex items-center justify-between p-2 rounded border hover:bg-[var(--muted)] transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--primary)] hover:underline font-medium text-sm"
>
{link.key}
</a>
<Badge
className={`text-xs ${
link.status === 'done'
? 'bg-[var(--success)]/10 text-[var(--success)]'
: 'bg-[var(--muted)]/50 text-[var(--muted-foreground)]'
}`}
>
{link.status}
</Badge>
</div>
<p className="text-xs text-[var(--muted-foreground)] truncate">
{link.title}
</p>
</div>
<div className="flex items-center gap-2 text-xs text-[var(--muted-foreground)]">
<span>{link.type}</span>
<span>{link.estimatedPoints}pts</span>
</div>
</div>
))}
</div>
</div>
{/* Insights business */}
{insights.length > 0 && (
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)]">
<h4 className="font-medium mb-2">💡 Insights business</h4>
<div className="text-sm text-[var(--muted-foreground)] space-y-1">
{insights.map((insight, index) => (
<p key={index}>{insight}</p>
))}
</div>
</div>
)}
{/* Note sur les story points */}
<div className="text-xs text-[var(--muted-foreground)] bg-[var(--card)] border border-[var(--border)] p-2 rounded">
<p>
* Story Points estimés automatiquement basés sur le type de ticket
(Epic: 8pts, Story: 3pts, Task: 2pts, Bug: 1pt)
</p>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,497 @@
'use client';
import { useState } from 'react';
import { ManagerSummary } from '@/services/manager-summary';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { getPriorityConfig } from '@/lib/status-config';
import { useTasksContext } from '@/contexts/TasksContext';
import { MetricsTab } from './MetricsTab';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
interface ManagerWeeklySummaryProps {
initialSummary: ManagerSummary;
}
export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySummaryProps) {
const [summary] = useState<ManagerSummary>(initialSummary);
const [activeView, setActiveView] = useState<'narrative' | 'accomplishments' | 'challenges' | 'metrics'>('narrative');
const { tags: availableTags } = useTasksContext();
const handleRefresh = () => {
// SSR - refresh via page reload
window.location.reload();
};
const formatPeriod = () => {
return `Semaine du ${format(summary.period.start, 'dd MMM', { locale: fr })} au ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })}`;
};
const getPriorityBadgeStyle = (priority: 'low' | 'medium' | 'high') => {
const config = getPriorityConfig(priority);
const baseClasses = 'text-xs px-2 py-0.5 rounded font-medium';
switch (config.color) {
case 'blue':
return `${baseClasses} bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400`;
case 'yellow':
return `${baseClasses} bg-yellow-100 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400`;
case 'purple':
return `${baseClasses} bg-purple-100 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400`;
case 'red':
return `${baseClasses} bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400`;
default:
return `${baseClasses} bg-gray-100 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400`;
}
};
return (
<div className="space-y-6">
{/* Header avec navigation */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-[var(--foreground)]">👔 Résumé Manager</h1>
<p className="text-[var(--muted-foreground)]">{formatPeriod()}</p>
</div>
<Button
onClick={handleRefresh}
variant="secondary"
size="sm"
>
🔄 Actualiser
</Button>
</div>
{/* Navigation des vues */}
<div className="border-b border-[var(--border)]">
<nav className="flex space-x-8">
<button
onClick={() => setActiveView('narrative')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeView === 'narrative'
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
📝 Vue Executive
</button>
<button
onClick={() => setActiveView('accomplishments')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeView === 'accomplishments'
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
Accomplissements ({summary.keyAccomplishments.length})
</button>
<button
onClick={() => setActiveView('challenges')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeView === 'challenges'
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
🎯 Enjeux à venir ({summary.upcomingChallenges.length})
</button>
<button
onClick={() => setActiveView('metrics')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeView === 'metrics'
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
📊 Métriques
</button>
</nav>
</div>
{/* Vue Executive / Narrative */}
{activeView === 'narrative' && (
<div className="space-y-6">
{/* Résumé narratif */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold flex items-center gap-2">
📊 Résumé de la semaine
</h2>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-400">
<h3 className="font-medium text-blue-900 mb-2">🎯 Points clés accomplis</h3>
<p className="text-blue-800">{summary.narrative.weekHighlight}</p>
</div>
<div className="bg-yellow-50 p-4 rounded-lg border-l-4 border-yellow-400">
<h3 className="font-medium text-yellow-900 mb-2"> Défis traités</h3>
<p className="text-yellow-800">{summary.narrative.mainChallenges}</p>
</div>
<div className="bg-green-50 p-4 rounded-lg border-l-4 border-green-400">
<h3 className="font-medium text-green-900 mb-2">🔮 Focus semaine prochaine</h3>
<p className="text-green-800">{summary.narrative.nextWeekFocus}</p>
</div>
</CardContent>
</Card>
{/* Métriques rapides */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">📈 Métriques en bref</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="text-center p-4 bg-blue-50 rounded-lg">
<div className="text-2xl font-bold text-blue-600">
{summary.metrics.totalTasksCompleted}
</div>
<div className="text-sm text-blue-600">Tâches complétées</div>
<div className="text-xs text-blue-500">
dont {summary.metrics.highPriorityTasksCompleted} priorité haute
</div>
</div>
<div className="text-center p-4 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-600">
{summary.metrics.totalCheckboxesCompleted}
</div>
<div className="text-sm text-green-600">Todos complétés</div>
<div className="text-xs text-green-500">
dont {summary.metrics.meetingCheckboxesCompleted} meetings
</div>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">
{summary.keyAccomplishments.filter(a => a.impact === 'high').length}
</div>
<div className="text-sm text-purple-600">Items à fort impact</div>
<div className="text-xs text-purple-500">
/ {summary.keyAccomplishments.length} accomplissements
</div>
</div>
<div className="text-center p-4 bg-orange-50 rounded-lg">
<div className="text-2xl font-bold text-orange-600">
{summary.upcomingChallenges.filter(c => c.priority === 'high').length}
</div>
<div className="text-sm text-orange-600">Priorités critiques</div>
<div className="text-xs text-orange-500">
/ {summary.upcomingChallenges.length} enjeux
</div>
</div>
</div>
</CardContent>
</Card>
{/* Top accomplissements */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">🏆 Top accomplissements</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{summary.keyAccomplishments.length === 0 ? (
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
<p>Aucun accomplissement significatif trouvé cette semaine.</p>
<p className="text-sm mt-2">Ajoutez des tâches avec priorité haute/medium ou des meetings.</p>
</div>
) : (
summary.keyAccomplishments.slice(0, 6).map((accomplishment, index) => (
<div
key={accomplishment.id}
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-xs font-bold flex items-center justify-center">
#{index + 1}
</span>
<span className={getPriorityBadgeStyle(accomplishment.impact)}>
{getPriorityConfig(accomplishment.impact).label}
</span>
</div>
<span className="text-xs text-[var(--muted-foreground)]">
{format(accomplishment.completedAt, 'dd/MM', { locale: fr })}
</span>
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{accomplishment.title}
</h4>
{/* Tags */}
{accomplishment.tags && accomplishment.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={accomplishment.tags}
availableTags={availableTags}
size="sm"
maxTags={2}
/>
</div>
)}
{/* Description si disponible */}
{accomplishment.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
{accomplishment.description}
</p>
)}
{/* Count de todos */}
{accomplishment.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>📋</span>
<span>{accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''}</span>
</div>
)}
</div>
))
)}
</div>
</CardContent>
</Card>
{/* Top challenges */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">🎯 Top enjeux à venir</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{summary.upcomingChallenges.length === 0 ? (
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
<p>Aucun enjeu prioritaire trouvé.</p>
<p className="text-sm mt-2">Ajoutez des tâches non complétées avec priorité haute/medium.</p>
</div>
) : (
summary.upcomingChallenges.slice(0, 6).map((challenge, index) => (
<div
key={challenge.id}
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-xs font-bold flex items-center justify-center">
#{index + 1}
</span>
<span className={getPriorityBadgeStyle(challenge.priority)}>
{getPriorityConfig(challenge.priority).label}
</span>
</div>
{challenge.deadline && (
<span className="text-xs text-[var(--muted-foreground)]">
{format(challenge.deadline, 'dd/MM', { locale: fr })}
</span>
)}
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{challenge.title}
</h4>
{/* Tags */}
{challenge.tags && challenge.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={challenge.tags}
availableTags={availableTags}
size="sm"
maxTags={2}
/>
</div>
)}
{/* Description si disponible */}
{challenge.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
{challenge.description}
</p>
)}
{/* Count de todos */}
{challenge.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>📋</span>
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
</div>
)}
</div>
))
)}
</div>
</CardContent>
</Card>
</div>
)}
{/* Vue détaillée des accomplissements */}
{activeView === 'accomplishments' && (
<Card>
<CardHeader>
<h2 className="text-lg font-semibold"> Accomplissements de la semaine</h2>
<p className="text-sm text-[var(--muted-foreground)]">
{summary.keyAccomplishments.length} accomplissements significatifs {summary.metrics.totalTasksCompleted} tâches {summary.metrics.totalCheckboxesCompleted} todos complétés
</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{summary.keyAccomplishments.map((accomplishment, index) => (
<div
key={accomplishment.id}
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-xs font-bold flex items-center justify-center">
#{index + 1}
</span>
<span className={getPriorityBadgeStyle(accomplishment.impact)}>
{getPriorityConfig(accomplishment.impact).label}
</span>
</div>
<span className="text-xs text-[var(--muted-foreground)]">
{format(accomplishment.completedAt, 'dd/MM', { locale: fr })}
</span>
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{accomplishment.title}
</h4>
{/* Tags */}
{accomplishment.tags && accomplishment.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={accomplishment.tags}
availableTags={availableTags}
size="sm"
maxTags={3}
/>
</div>
)}
{/* Description si disponible */}
{accomplishment.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-3 leading-relaxed mb-2">
{accomplishment.description}
</p>
)}
{/* Count de todos */}
{accomplishment.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>📋</span>
<span>{accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''}</span>
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Vue détaillée des challenges */}
{activeView === 'challenges' && (
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">🎯 Enjeux et défis à venir</h2>
<p className="text-sm text-[var(--muted-foreground)]">
{summary.upcomingChallenges.length} défis identifiés {summary.upcomingChallenges.filter(c => c.priority === 'high').length} priorité haute {summary.upcomingChallenges.filter(c => c.blockers.length > 0).length} avec blockers
</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{summary.upcomingChallenges.map((challenge, index) => (
<div
key={challenge.id}
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-xs font-bold flex items-center justify-center">
#{index + 1}
</span>
<span className={getPriorityBadgeStyle(challenge.priority)}>
{getPriorityConfig(challenge.priority).label}
</span>
</div>
{challenge.deadline && (
<span className="text-xs text-[var(--muted-foreground)]">
{format(challenge.deadline, 'dd/MM', { locale: fr })}
</span>
)}
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{challenge.title}
</h4>
{/* Tags */}
{challenge.tags && challenge.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={challenge.tags}
availableTags={availableTags}
size="sm"
maxTags={3}
/>
</div>
)}
{/* Description si disponible */}
{challenge.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-3 leading-relaxed mb-2">
{challenge.description}
</p>
)}
{/* Count de todos */}
{challenge.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>📋</span>
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Vue Métriques */}
{activeView === 'metrics' && (
<MetricsTab />
)}
</div>
);
}

View File

@@ -0,0 +1,257 @@
'use client';
import { useState } from 'react';
import { useWeeklyMetrics, useVelocityTrends } from '@/hooks/use-metrics';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { DailyStatusChart } from './charts/DailyStatusChart';
import { CompletionRateChart } from './charts/CompletionRateChart';
import { StatusDistributionChart } from './charts/StatusDistributionChart';
import { PriorityBreakdownChart } from './charts/PriorityBreakdownChart';
import { VelocityTrendChart } from './charts/VelocityTrendChart';
import { WeeklyActivityHeatmap } from './charts/WeeklyActivityHeatmap';
import { ProductivityInsights } from './charts/ProductivityInsights';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
interface MetricsTabProps {
className?: string;
}
export function MetricsTab({ className }: MetricsTabProps) {
const [selectedDate] = useState<Date>(new Date());
const [weeksBack, setWeeksBack] = useState(4);
const { metrics, loading: metricsLoading, error: metricsError, refetch: refetchMetrics } = useWeeklyMetrics(selectedDate);
const { trends, loading: trendsLoading, error: trendsError, refetch: refetchTrends } = useVelocityTrends(weeksBack);
const handleRefresh = () => {
refetchMetrics();
refetchTrends();
};
const formatPeriod = () => {
if (!metrics) return '';
return `Semaine du ${format(metrics.period.start, 'dd MMM', { locale: fr })} au ${format(metrics.period.end, 'dd MMM yyyy', { locale: fr })}`;
};
const getTrendIcon = (trend: string) => {
switch (trend) {
case 'improving': return '📈';
case 'declining': return '📉';
case 'stable': return '➡️';
default: return '📊';
}
};
const getPatternIcon = (pattern: string) => {
switch (pattern) {
case 'consistent': return '🎯';
case 'variable': return '📊';
case 'weekend-heavy': return '📅';
default: return '📋';
}
};
if (metricsError || trendsError) {
return (
<div className={className}>
<Card>
<CardContent className="p-6 text-center">
<p className="text-red-500 mb-4">
Erreur lors du chargement des métriques
</p>
<p className="text-sm text-[var(--muted-foreground)] mb-4">
{metricsError || trendsError}
</p>
<Button onClick={handleRefresh} variant="secondary" size="sm">
🔄 Réessayer
</Button>
</CardContent>
</Card>
</div>
);
}
return (
<div className={className}>
{/* Header avec période et contrôles */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-[var(--foreground)]">📊 Métriques & Analytics</h2>
<p className="text-[var(--muted-foreground)]">{formatPeriod()}</p>
</div>
<div className="flex items-center gap-2">
<Button
onClick={handleRefresh}
variant="secondary"
size="sm"
disabled={metricsLoading || trendsLoading}
>
🔄 Actualiser
</Button>
</div>
</div>
{metricsLoading ? (
<Card>
<CardContent className="p-6 text-center">
<div className="animate-pulse">
<div className="h-4 bg-[var(--border)] rounded w-1/4 mx-auto mb-4"></div>
<div className="h-32 bg-[var(--border)] rounded"></div>
</div>
<p className="text-[var(--muted-foreground)] mt-4">Chargement des métriques...</p>
</CardContent>
</Card>
) : metrics ? (
<div className="space-y-6">
{/* Vue d'ensemble rapide */}
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🎯 Vue d&apos;ensemble</h3>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
<div className="text-center p-4 bg-green-50 dark:bg-green-950/20 rounded-lg">
<div className="text-2xl font-bold text-green-600">
{metrics.summary.totalTasksCompleted}
</div>
<div className="text-sm text-green-600">Terminées</div>
</div>
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg">
<div className="text-2xl font-bold text-blue-600">
{metrics.summary.totalTasksCreated}
</div>
<div className="text-sm text-blue-600">Créées</div>
</div>
<div className="text-center p-4 bg-purple-50 dark:bg-purple-950/20 rounded-lg">
<div className="text-2xl font-bold text-purple-600">
{metrics.summary.averageCompletionRate.toFixed(1)}%
</div>
<div className="text-sm text-purple-600">Taux moyen</div>
</div>
<div className="text-center p-4 bg-orange-50 dark:bg-orange-950/20 rounded-lg">
<div className="text-2xl font-bold text-orange-600">
{getTrendIcon(metrics.summary.trendsAnalysis.completionTrend)}
</div>
<div className="text-sm text-orange-600 capitalize">
{metrics.summary.trendsAnalysis.completionTrend}
</div>
</div>
<div className="text-center p-4 bg-gray-50 dark:bg-gray-950/20 rounded-lg">
<div className="text-2xl font-bold text-gray-600">
{getPatternIcon(metrics.summary.trendsAnalysis.productivityPattern)}
</div>
<div className="text-sm text-gray-600">
{metrics.summary.trendsAnalysis.productivityPattern === 'consistent' ? 'Régulier' :
metrics.summary.trendsAnalysis.productivityPattern === 'variable' ? 'Variable' : 'Weekend+'}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Graphiques principaux */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">📈 Évolution quotidienne des statuts</h3>
</CardHeader>
<CardContent>
<DailyStatusChart data={metrics.dailyBreakdown} />
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🎯 Taux de completion quotidien</h3>
</CardHeader>
<CardContent>
<CompletionRateChart data={metrics.dailyBreakdown} />
</CardContent>
</Card>
</div>
{/* Distribution et priorités */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🍰 Répartition des statuts</h3>
</CardHeader>
<CardContent>
<StatusDistributionChart data={metrics.statusDistribution} />
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-lg font-semibold"> Performance par priorité</h3>
</CardHeader>
<CardContent>
<PriorityBreakdownChart data={metrics.priorityBreakdown} />
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🔥 Heatmap d&apos;activité</h3>
</CardHeader>
<CardContent>
<WeeklyActivityHeatmap data={metrics.dailyBreakdown} />
</CardContent>
</Card>
</div>
{/* Tendances de vélocité */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">🚀 Tendances de vélocité</h3>
<select
value={weeksBack}
onChange={(e) => setWeeksBack(parseInt(e.target.value))}
className="text-sm border border-[var(--border)] rounded px-2 py-1 bg-[var(--background)]"
disabled={trendsLoading}
>
<option value={4}>4 semaines</option>
<option value={8}>8 semaines</option>
<option value={12}>12 semaines</option>
</select>
</div>
</CardHeader>
<CardContent>
{trendsLoading ? (
<div className="h-[300px] flex items-center justify-center">
<div className="animate-pulse text-center">
<div className="h-4 bg-[var(--border)] rounded w-32 mx-auto mb-2"></div>
<div className="h-48 bg-[var(--border)] rounded"></div>
</div>
</div>
) : trends.length > 0 ? (
<VelocityTrendChart data={trends} />
) : (
<div className="h-[300px] flex items-center justify-center text-[var(--muted-foreground)]">
Aucune donnée de vélocité disponible
</div>
)}
</CardContent>
</Card>
{/* Analyses de productivité */}
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">💡 Analyses de productivité</h3>
</CardHeader>
<CardContent>
<ProductivityInsights data={metrics.dailyBreakdown} />
</CardContent>
</Card>
</div>
) : null}
</div>
);
}

View 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>
);
}

View File

@@ -0,0 +1,93 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
import { CreateTaskData } from '@/clients/tasks-client';
import Link from 'next/link';
interface QuickActionsProps {
onCreateTask: (data: CreateTaskData) => Promise<void>;
}
export function QuickActions({ onCreateTask }: QuickActionsProps) {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const handleCreateTask = async (data: CreateTaskData) => {
await onCreateTask(data);
setIsCreateModalOpen(false);
};
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<Button
variant="primary"
onClick={() => setIsCreateModalOpen(true)}
className="flex items-center gap-2 p-6 h-auto"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<div className="text-left">
<div className="font-semibold">Nouvelle Tâche</div>
<div className="text-sm opacity-80">Créer une nouvelle tâche</div>
</div>
</Button>
<Link href="/kanban">
<Button
variant="secondary"
className="flex items-center gap-2 p-6 h-auto w-full"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V5a2 2 0 012-2h2a2 2 0 002-2" />
</svg>
<div className="text-left">
<div className="font-semibold">Kanban Board</div>
<div className="text-sm opacity-80">Gérer les tâches</div>
</div>
</Button>
</Link>
<Link href="/daily">
<Button
variant="secondary"
className="flex items-center gap-2 p-6 h-auto w-full"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<div className="text-left">
<div className="font-semibold">Daily</div>
<div className="text-sm opacity-80">Checkboxes quotidiennes</div>
</div>
</Button>
</Link>
<Link href="/settings">
<Button
variant="secondary"
className="flex items-center gap-2 p-6 h-auto w-full"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<div className="text-left">
<div className="font-semibold">Paramètres</div>
<div className="text-sm opacity-80">Configuration</div>
</div>
</Button>
</Link>
</div>
<CreateTaskForm
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSubmit={handleCreateTask}
loading={false}
/>
</>
);
}

View File

@@ -0,0 +1,131 @@
'use client';
import { Task } from '@/lib/types';
import { Card } from '@/components/ui/Card';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { Badge } from '@/components/ui/Badge';
import { useTasksContext } from '@/contexts/TasksContext';
import { getPriorityConfig, getPriorityColorHex, getStatusBadgeClasses, getStatusLabel } from '@/lib/status-config';
import { TaskPriority } from '@/lib/types';
import Link from 'next/link';
interface RecentTasksProps {
tasks: Task[];
}
export function RecentTasks({ tasks }: RecentTasksProps) {
const { tags: availableTags } = useTasksContext();
// Prendre les 5 tâches les plus récentes (créées ou modifiées)
const recentTasks = tasks
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
.slice(0, 5);
// Fonctions simplifiées utilisant la configuration centralisée
const getPriorityStyle = (priority: string) => {
try {
const config = getPriorityConfig(priority as TaskPriority);
const hexColor = getPriorityColorHex(config.color);
return { color: hexColor };
} catch {
return { color: '#6b7280' }; // gray-500 par défaut
}
};
return (
<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">
<button className="text-sm text-[var(--primary)] hover:underline">
Voir toutes
</button>
</Link>
</div>
{recentTasks.length === 0 ? (
<div className="text-center py-8 text-[var(--muted-foreground)]">
<svg className="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p>Aucune tâche disponible</p>
<p className="text-sm">Créez votre première tâche pour commencer</p>
</div>
) : (
<div className="space-y-3">
{recentTasks.map((task) => (
<div
key={task.id}
className="p-3 border border-[var(--border)] rounded-lg hover:bg-[var(--card)]/50 transition-colors"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-sm truncate">{task.title}</h4>
{task.source === 'jira' && (
<Badge variant="outline" className="text-xs">
Jira
</Badge>
)}
</div>
{task.description && (
<p className="text-xs text-[var(--muted-foreground)] mb-2 line-clamp-1">
{task.description}
</p>
)}
<div className="flex items-center gap-2 flex-wrap">
<Badge className={`text-xs ${getStatusBadgeClasses(task.status)}`}>
{getStatusLabel(task.status)}
</Badge>
{task.priority && (
<span
className="text-xs font-medium"
style={getPriorityStyle(task.priority)}
>
{(() => {
try {
return getPriorityConfig(task.priority as TaskPriority).label;
} catch {
return task.priority;
}
})()}
</span>
)}
{task.tags && task.tags.length > 0 && (
<div className="flex gap-1">
<TagDisplay
tags={task.tags.slice(0, 2)}
availableTags={availableTags}
size="sm"
maxTags={2}
showColors={true}
/>
{task.tags.length > 2 && (
<span className="text-xs text-[var(--muted-foreground)]">
+{task.tags.length - 2}
</span>
)}
</div>
)}
</div>
</div>
<div className="text-xs text-[var(--muted-foreground)] whitespace-nowrap">
{new Date(task.updatedAt).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short'
})}
</div>
</div>
</div>
))}
</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,97 @@
'use client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { DailyMetrics } from '@/services/metrics';
interface CompletionRateChartProps {
data: DailyMetrics[];
className?: string;
}
export function CompletionRateChart({ data, className }: CompletionRateChartProps) {
// Transformer les données pour le graphique
const chartData = data.map(day => ({
day: day.dayName.substring(0, 3), // Lun, Mar, etc.
date: new Date(day.date).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }),
completionRate: day.completionRate,
completed: day.completed,
total: day.totalTasks
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: any[]; label?: string }) => {
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="font-medium mb-2">{`${label} (${data.date})`}</p>
<p className="text-sm text-[var(--foreground)]">
Taux de completion: {data.completionRate.toFixed(1)}%
</p>
<p className="text-sm text-[var(--muted-foreground)]">
{data.completed} / {data.total} tâches
</p>
</div>
);
}
return null;
};
// Calculer la moyenne pour la ligne de référence
const averageRate = data.reduce((sum, day) => sum + day.completionRate, 0) / data.length;
return (
<div className={className}>
<ResponsiveContainer width="100%" height={250}>
<LineChart
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="day"
stroke="var(--muted-foreground)"
fontSize={12}
/>
<YAxis
stroke="var(--muted-foreground)"
fontSize={12}
domain={[0, 100]}
tickFormatter={(value) => `${value}%`}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="completionRate"
stroke="#10b981"
strokeWidth={3}
dot={{ fill: "#10b981", strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: "#10b981", strokeWidth: 2 }}
/>
{/* Ligne de moyenne */}
<Line
type="monotone"
dataKey={() => averageRate}
stroke="#94a3b8"
strokeWidth={1}
strokeDasharray="5 5"
dot={false}
activeDot={false}
/>
</LineChart>
</ResponsiveContainer>
{/* Légende */}
<div className="flex items-center justify-center gap-4 mt-2 text-xs text-[var(--muted-foreground)]">
<div className="flex items-center gap-1">
<div className="w-3 h-0.5 bg-green-500"></div>
<span>Taux quotidien</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-0.5 bg-gray-400 border-dashed"></div>
<span>Moyenne ({averageRate.toFixed(1)}%)</span>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,68 @@
'use client';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { DailyMetrics } from '@/services/metrics';
interface DailyStatusChartProps {
data: DailyMetrics[];
className?: string;
}
export function DailyStatusChart({ data, className }: DailyStatusChartProps) {
// Transformer les données pour le graphique
const chartData = data.map(day => ({
day: day.dayName.substring(0, 3), // Lun, Mar, etc.
date: new Date(day.date).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }),
'Complétées': day.completed,
'En cours': day.inProgress,
'Bloquées': day.blocked,
'En attente': day.pending,
'Nouvelles': day.newTasks
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: any[]; 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="font-medium mb-2">{`${label} (${payload[0]?.payload?.date})`}</p>
{payload.map((entry: { dataKey: string; value: number; color: string }, index: number) => (
<p key={index} style={{ color: entry.color }} className="text-sm">
{`${entry.dataKey}: ${entry.value}`}
</p>
))}
</div>
);
}
return null;
};
return (
<div className={className}>
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="day"
stroke="var(--muted-foreground)"
fontSize={12}
/>
<YAxis
stroke="var(--muted-foreground)"
fontSize={12}
/>
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar dataKey="Complétées" fill="#10b981" radius={[2, 2, 0, 0]} />
<Bar dataKey="En cours" fill="#3b82f6" radius={[2, 2, 0, 0]} />
<Bar dataKey="Bloquées" fill="#ef4444" radius={[2, 2, 0, 0]} />
<Bar dataKey="En attente" fill="#94a3b8" radius={[2, 2, 0, 0]} />
<Bar dataKey="Nouvelles" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
);
}

View File

@@ -0,0 +1,112 @@
'use client';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
interface PriorityData {
priority: string;
completed: number;
pending: number;
total: number;
completionRate: number;
color: string;
}
interface PriorityBreakdownChartProps {
data: PriorityData[];
className?: string;
}
export function PriorityBreakdownChart({ data, className }: PriorityBreakdownChartProps) {
// Transformer les données pour l'affichage
const getPriorityLabel = (priority: string) => {
const labels: { [key: string]: string } = {
'high': 'Haute',
'medium': 'Moyenne',
'low': 'Basse'
};
return labels[priority] || priority;
};
const chartData = data.map(item => ({
priority: getPriorityLabel(item.priority),
'Terminées': item.completed,
'En cours': item.pending,
completionRate: item.completionRate,
total: item.total
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: any[]; label?: string }) => {
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="font-medium mb-2">{`Priorité ${label}`}</p>
<p className="text-sm text-green-600">
Terminées: {data['Terminées']}
</p>
<p className="text-sm text-blue-600">
En cours: {data['En cours']}
</p>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
Taux: {data.completionRate.toFixed(1)}% ({data.total} total)
</p>
</div>
);
}
return null;
};
return (
<div className={className}>
<ResponsiveContainer width="100%" height={250}>
<BarChart
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="priority"
stroke="var(--muted-foreground)"
fontSize={12}
/>
<YAxis
stroke="var(--muted-foreground)"
fontSize={12}
/>
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar
dataKey="Terminées"
stackId="a"
fill="#10b981"
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="En cours"
stackId="a"
fill="#3b82f6"
radius={[2, 2, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
{/* Affichage des taux de completion */}
<div className="mt-4 grid grid-cols-3 gap-4 text-center">
{data.map((item, index) => (
<div key={index} className="p-2 bg-[var(--card)] rounded border">
<div className="text-xs text-[var(--muted-foreground)] mb-1">
{getPriorityLabel(item.priority)}
</div>
<div className="text-lg font-bold" style={{ color: item.color }}>
{item.completionRate.toFixed(0)}%
</div>
<div className="text-xs text-[var(--muted-foreground)]">
{item.completed}/{item.total}
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,190 @@
'use client';
import { DailyMetrics } from '@/services/metrics';
interface ProductivityInsightsProps {
data: DailyMetrics[];
className?: string;
}
export function ProductivityInsights({ data, className }: ProductivityInsightsProps) {
// Calculer les insights
const totalCompleted = data.reduce((sum, day) => sum + day.completed, 0);
const totalCreated = data.reduce((sum, day) => sum + day.newTasks, 0);
// const averageCompletion = data.reduce((sum, day) => sum + day.completionRate, 0) / data.length;
// Trouver le jour le plus productif
const mostProductiveDay = data.reduce((best, day) =>
day.completed > best.completed ? day : best
);
// Trouver le jour avec le plus de nouvelles tâches
const mostCreativeDay = data.reduce((best, day) =>
day.newTasks > best.newTasks ? day : best
);
// Analyser la tendance
const firstHalf = data.slice(0, Math.ceil(data.length / 2));
const secondHalf = data.slice(Math.ceil(data.length / 2));
const firstHalfAvg = firstHalf.reduce((sum, day) => sum + day.completed, 0) / firstHalf.length;
const secondHalfAvg = secondHalf.reduce((sum, day) => sum + day.completed, 0) / secondHalf.length;
const trend = secondHalfAvg > firstHalfAvg ? 'up' : secondHalfAvg < firstHalfAvg ? 'down' : 'stable';
// Calculer la consistance (écart-type faible = plus consistant)
const avgCompleted = totalCompleted / data.length;
const variance = data.reduce((sum, day) => {
const diff = day.completed - avgCompleted;
return sum + diff * diff;
}, 0) / data.length;
const standardDeviation = Math.sqrt(variance);
const consistencyScore = Math.max(0, 100 - (standardDeviation * 10)); // Score sur 100
// Ratio création/completion
const creationRatio = totalCreated > 0 ? (totalCompleted / totalCreated) * 100 : 0;
const getTrendIcon = () => {
switch (trend) {
case 'up': return { icon: '📈', color: 'text-green-600', label: 'En amélioration' };
case 'down': return { icon: '📉', color: 'text-red-600', label: 'En baisse' };
default: return { icon: '➡️', color: 'text-blue-600', label: 'Stable' };
}
};
const getConsistencyLevel = () => {
if (consistencyScore >= 80) return { label: 'Très régulier', color: 'text-green-600', icon: '🎯' };
if (consistencyScore >= 60) return { label: 'Assez régulier', color: 'text-blue-600', icon: '📊' };
if (consistencyScore >= 40) return { label: 'Variable', color: 'text-yellow-600', icon: '📊' };
return { label: 'Très variable', color: 'text-red-600', icon: '📊' };
};
const getRatioStatus = () => {
if (creationRatio >= 100) return { label: 'Équilibré+', color: 'text-green-600', icon: '⚖️' };
if (creationRatio >= 80) return { label: 'Bien équilibré', color: 'text-blue-600', icon: '⚖️' };
if (creationRatio >= 60) return { label: 'Légèrement en retard', color: 'text-yellow-600', icon: '⚖️' };
return { label: 'Accumulation', color: 'text-red-600', icon: '⚖️' };
};
const trendInfo = getTrendIcon();
const consistencyInfo = getConsistencyLevel();
const ratioInfo = getRatioStatus();
return (
<div className={className}>
<div className="space-y-4">
{/* Insights principaux */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Jour le plus productif */}
<div className="p-4 bg-green-50 dark:bg-green-950/20 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-green-900 dark:text-green-100">
🏆 Jour champion
</h4>
<span className="text-2xl font-bold text-green-600">
{mostProductiveDay.completed}
</span>
</div>
<p className="text-sm text-green-800 dark:text-green-200">
{mostProductiveDay.dayName} - {mostProductiveDay.completed} tâches terminées
</p>
<p className="text-xs text-green-600 mt-1">
Taux: {mostProductiveDay.completionRate.toFixed(1)}%
</p>
</div>
{/* Jour le plus créatif */}
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-blue-900 dark:text-blue-100">
💡 Jour créatif
</h4>
<span className="text-2xl font-bold text-blue-600">
{mostCreativeDay.newTasks}
</span>
</div>
<p className="text-sm text-blue-800 dark:text-blue-200">
{mostCreativeDay.dayName} - {mostCreativeDay.newTasks} nouvelles tâches
</p>
<p className="text-xs text-blue-600 mt-1">
{mostCreativeDay.dayName === mostProductiveDay.dayName ?
'Également jour le plus productif!' :
'Journée de planification'}
</p>
</div>
</div>
{/* Analyses comportementales */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Tendance */}
<div className="p-4 bg-[var(--card)] border border-[var(--border)] rounded-lg">
<div className="flex items-center gap-2 mb-2">
<span className="text-2xl">{trendInfo.icon}</span>
<h4 className="font-medium text-[var(--foreground)]">Tendance</h4>
</div>
<p className={`text-sm font-medium ${trendInfo.color}`}>
{trendInfo.label}
</p>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
{secondHalfAvg > firstHalfAvg ?
`+${(((secondHalfAvg - firstHalfAvg) / firstHalfAvg) * 100).toFixed(1)}%` :
`${(((secondHalfAvg - firstHalfAvg) / firstHalfAvg) * 100).toFixed(1)}%`}
</p>
</div>
{/* Consistance */}
<div className="p-4 bg-[var(--card)] border border-[var(--border)] rounded-lg">
<div className="flex items-center gap-2 mb-2">
<span className="text-2xl">{consistencyInfo.icon}</span>
<h4 className="font-medium text-[var(--foreground)]">Régularité</h4>
</div>
<p className={`text-sm font-medium ${consistencyInfo.color}`}>
{consistencyInfo.label}
</p>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
Score: {consistencyScore.toFixed(0)}/100
</p>
</div>
{/* Ratio Création/Completion */}
<div className="p-4 bg-[var(--card)] border border-[var(--border)] rounded-lg">
<div className="flex items-center gap-2 mb-2">
<span className="text-2xl">{ratioInfo.icon}</span>
<h4 className="font-medium text-[var(--foreground)]">Équilibre</h4>
</div>
<p className={`text-sm font-medium ${ratioInfo.color}`}>
{ratioInfo.label}
</p>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
{creationRatio.toFixed(0)}% de completion
</p>
</div>
</div>
{/* Recommandations */}
<div className="p-4 bg-yellow-50 dark:bg-yellow-950/20 rounded-lg">
<h4 className="font-medium text-yellow-900 dark:text-yellow-100 mb-2 flex items-center gap-2">
💡 Recommandations
</h4>
<div className="space-y-1 text-sm text-yellow-800 dark:text-yellow-200">
{trend === 'down' && (
<p> Essayez de retrouver votre rythme du début de semaine</p>
)}
{consistencyScore < 60 && (
<p> Essayez de maintenir un rythme plus régulier</p>
)}
{creationRatio < 80 && (
<p> Concentrez-vous plus sur terminer les tâches existantes</p>
)}
{creationRatio > 120 && (
<p> Excellent rythme! Peut-être ralentir la création de nouvelles tâches</p>
)}
{mostProductiveDay.dayName === mostCreativeDay.dayName && (
<p> Excellente synergie création/exécution le {mostProductiveDay.dayName}</p>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,109 @@
'use client';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
interface StatusDistributionData {
status: string;
count: number;
percentage: number;
color: string;
}
interface StatusDistributionChartProps {
data: StatusDistributionData[];
className?: string;
}
export function StatusDistributionChart({ data, className }: StatusDistributionChartProps) {
// Transformer les statuts pour l'affichage
const getStatusLabel = (status: string) => {
const labels: { [key: string]: string } = {
'pending': 'En attente',
'in_progress': 'En cours',
'blocked': 'Bloquées',
'done': 'Terminées',
'archived': 'Archivées'
};
return labels[status] || status;
};
const chartData = data.map(item => ({
...item,
name: getStatusLabel(item.status),
value: item.count
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: any[] }) => {
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="font-medium mb-1">{data.name}</p>
<p className="text-sm text-[var(--foreground)]">
{data.count} tâches ({data.percentage}%)
</p>
</div>
);
}
return null;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomLabel = (props: any) => {
const { cx, cy, midAngle, innerRadius, outerRadius, percent } = props;
if (percent < 0.05) return null; // Ne pas afficher les labels pour les petites sections
const RADIAN = Math.PI / 180;
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
return (
<text
x={x}
y={y}
fill="white"
textAnchor={x > cx ? 'start' : 'end'}
dominantBaseline="central"
fontSize={12}
fontWeight="medium"
>
{`${(percent * 100).toFixed(0)}%`}
</text>
);
};
return (
<div className={className}>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={CustomLabel}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
<Legend
verticalAlign="bottom"
height={36}
formatter={(value, entry: { color?: string }) => (
<span style={{ color: entry.color, fontSize: '12px' }}>
{value}
</span>
)}
/>
</PieChart>
</ResponsiveContainer>
</div>
);
}

View File

@@ -0,0 +1,95 @@
'use client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { VelocityTrend } from '@/services/metrics';
interface VelocityTrendChartProps {
data: VelocityTrend[];
className?: string;
}
export function VelocityTrendChart({ data, className }: VelocityTrendChartProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: any[]; label?: string }) => {
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="font-medium mb-2">{`Semaine du ${label}`}</p>
<p className="text-sm text-green-600">
Terminées: {data.completed}
</p>
<p className="text-sm text-blue-600">
Créées: {data.created}
</p>
<p className="text-sm text-purple-600">
Vélocité: {data.velocity.toFixed(1)}%
</p>
</div>
);
}
return null;
};
return (
<div className={className}>
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={data}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="date"
stroke="var(--muted-foreground)"
fontSize={12}
/>
<YAxis
yAxisId="count"
stroke="var(--muted-foreground)"
fontSize={12}
orientation="left"
/>
<YAxis
yAxisId="velocity"
stroke="var(--muted-foreground)"
fontSize={12}
orientation="right"
domain={[0, 100]}
tickFormatter={(value) => `${value}%`}
/>
<Tooltip content={<CustomTooltip />} />
<Legend />
<Line
yAxisId="count"
type="monotone"
dataKey="completed"
stroke="#10b981"
strokeWidth={2}
dot={{ fill: "#10b981", strokeWidth: 2, r: 4 }}
name="Terminées"
/>
<Line
yAxisId="count"
type="monotone"
dataKey="created"
stroke="#3b82f6"
strokeWidth={2}
dot={{ fill: "#3b82f6", strokeWidth: 2, r: 4 }}
name="Créées"
/>
<Line
yAxisId="velocity"
type="monotone"
dataKey="velocity"
stroke="#8b5cf6"
strokeWidth={3}
dot={{ fill: "#8b5cf6", strokeWidth: 2, r: 5 }}
name="Vélocité (%)"
strokeDasharray="5 5"
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}

View File

@@ -0,0 +1,123 @@
'use client';
import { DailyMetrics } from '@/services/metrics';
interface WeeklyActivityHeatmapProps {
data: DailyMetrics[];
className?: string;
}
export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmapProps) {
// Calculer l'intensité max pour la normalisation
const maxActivity = Math.max(...data.map(day => day.completed + day.newTasks));
// Obtenir l'intensité relative (0-1)
const getIntensity = (day: DailyMetrics) => {
const activity = day.completed + day.newTasks;
return maxActivity > 0 ? activity / maxActivity : 0;
};
// Obtenir la couleur basée sur l'intensité
const getColorClass = (intensity: number) => {
if (intensity === 0) return 'bg-gray-100 dark:bg-gray-800';
if (intensity < 0.2) return 'bg-green-100 dark:bg-green-900/30';
if (intensity < 0.4) return 'bg-green-200 dark:bg-green-800/50';
if (intensity < 0.6) return 'bg-green-300 dark:bg-green-700/70';
if (intensity < 0.8) return 'bg-green-400 dark:bg-green-600/80';
return 'bg-green-500 dark:bg-green-500';
};
return (
<div className={className}>
<div className="space-y-4">
{/* Titre */}
<div className="text-center">
<h4 className="text-sm font-medium text-[var(--foreground)] mb-2">
Heatmap d&apos;activité hebdomadaire
</h4>
<p className="text-xs text-[var(--muted-foreground)]">
Intensité basée sur les tâches complétées + nouvelles tâches
</p>
</div>
{/* Heatmap */}
<div className="flex justify-center">
<div className="flex gap-1">
{data.map((day, index) => {
const intensity = getIntensity(day);
const colorClass = getColorClass(intensity);
const totalActivity = day.completed + day.newTasks;
return (
<div key={index} className="text-center">
{/* Carré de couleur */}
<div
className={`w-8 h-8 rounded ${colorClass} border border-[var(--border)] flex items-center justify-center transition-all hover:scale-110 cursor-help group relative`}
title={`${day.dayName}: ${totalActivity} activités (${day.completed} complétées, ${day.newTasks} créées)`}
>
{/* Tooltip au hover */}
<div className="opacity-0 group-hover:opacity-100 absolute bottom-10 left-1/2 transform -translate-x-1/2 bg-[var(--card)] border border-[var(--border)] rounded p-2 text-xs whitespace-nowrap z-10 shadow-lg transition-opacity">
<div className="font-medium">{day.dayName}</div>
<div className="text-[var(--muted-foreground)]">
{day.completed} terminées, {day.newTasks} créées
</div>
<div className="text-[var(--muted-foreground)]">
Taux: {day.completionRate.toFixed(1)}%
</div>
</div>
{/* Indicator si jour actuel */}
{new Date(day.date).toDateString() === new Date().toDateString() && (
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
)}
</div>
{/* Label du jour */}
<div className="text-xs text-[var(--muted-foreground)] mt-1">
{day.dayName.substring(0, 3)}
</div>
</div>
);
})}
</div>
</div>
{/* Légende */}
<div className="flex items-center justify-center gap-2 text-xs text-[var(--muted-foreground)]">
<span>Moins</span>
<div className="flex gap-1">
<div className="w-3 h-3 bg-gray-100 dark:bg-gray-800 border border-[var(--border)] rounded"></div>
<div className="w-3 h-3 bg-green-100 dark:bg-green-900/30 border border-[var(--border)] rounded"></div>
<div className="w-3 h-3 bg-green-200 dark:bg-green-800/50 border border-[var(--border)] rounded"></div>
<div className="w-3 h-3 bg-green-300 dark:bg-green-700/70 border border-[var(--border)] rounded"></div>
<div className="w-3 h-3 bg-green-400 dark:bg-green-600/80 border border-[var(--border)] rounded"></div>
<div className="w-3 h-3 bg-green-500 dark:bg-green-500 border border-[var(--border)] rounded"></div>
</div>
<span>Plus</span>
</div>
{/* Stats rapides */}
<div className="grid grid-cols-3 gap-2 text-center text-xs">
<div className="p-2 bg-[var(--card)] rounded border">
<div className="font-medium text-green-600">
{data.reduce((sum, day) => sum + day.completed, 0)}
</div>
<div className="text-[var(--muted-foreground)]">Terminées</div>
</div>
<div className="p-2 bg-[var(--card)] rounded border">
<div className="font-medium text-blue-600">
{data.reduce((sum, day) => sum + day.newTasks, 0)}
</div>
<div className="text-[var(--muted-foreground)]">Créées</div>
</div>
<div className="p-2 bg-[var(--card)] rounded border">
<div className="font-medium text-purple-600">
{(data.reduce((sum, day) => sum + day.completionRate, 0) / data.length).toFixed(1)}%
</div>
<div className="text-[var(--muted-foreground)]">Taux moyen</div>
</div>
</div>
</div>
</div>
);
}