feat: enhance HomePage with tag metrics and analytics integration
- Added TagAnalyticsService to fetch tag distribution metrics for the HomePage. - Updated HomePageClient and ProductivityAnalytics components to utilize new tag metrics. - Refactored TagsClient to use utility functions for color validation and generation. - Simplified TagForm to use centralized tag colors from TAG_COLORS.
This commit is contained in:
@@ -13,6 +13,7 @@ import { WelcomeSection } from '@/components/dashboard/WelcomeSection';
|
||||
import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
|
||||
import { ProductivityMetrics } from '@/services/analytics/analytics';
|
||||
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
|
||||
import { TagDistributionMetrics } from '@/services/analytics/tag-analytics';
|
||||
import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts';
|
||||
|
||||
interface HomePageClientProps {
|
||||
@@ -21,12 +22,14 @@ interface HomePageClientProps {
|
||||
initialStats: TaskStats;
|
||||
productivityMetrics: ProductivityMetrics;
|
||||
deadlineMetrics: DeadlineMetrics;
|
||||
tagMetrics: TagDistributionMetrics;
|
||||
}
|
||||
|
||||
|
||||
function HomePageContent({ productivityMetrics, deadlineMetrics }: {
|
||||
function HomePageContent({ productivityMetrics, deadlineMetrics, tagMetrics }: {
|
||||
productivityMetrics: ProductivityMetrics;
|
||||
deadlineMetrics: DeadlineMetrics;
|
||||
tagMetrics: TagDistributionMetrics;
|
||||
}) {
|
||||
const { syncing, createTask, tasks } = useTasksContext();
|
||||
const [selectedSources, setSelectedSources] = useState<string[]>([]);
|
||||
@@ -83,6 +86,7 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: {
|
||||
<ProductivityAnalytics
|
||||
metrics={productivityMetrics}
|
||||
deadlineMetrics={deadlineMetrics}
|
||||
tagMetrics={tagMetrics}
|
||||
selectedSources={selectedSources}
|
||||
hiddenSources={hiddenSources}
|
||||
/>
|
||||
@@ -99,7 +103,8 @@ export function HomePageClient({
|
||||
initialTags,
|
||||
initialStats,
|
||||
productivityMetrics,
|
||||
deadlineMetrics
|
||||
deadlineMetrics,
|
||||
tagMetrics
|
||||
}: HomePageClientProps) {
|
||||
return (
|
||||
<TasksProvider
|
||||
@@ -110,6 +115,7 @@ export function HomePageClient({
|
||||
<HomePageContent
|
||||
productivityMetrics={productivityMetrics}
|
||||
deadlineMetrics={deadlineMetrics}
|
||||
tagMetrics={tagMetrics}
|
||||
/>
|
||||
</TasksProvider>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ProductivityMetrics } from '@/services/analytics/analytics';
|
||||
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
|
||||
import { TagDistributionMetrics } from '@/services/analytics/tag-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 { TagDistributionChart } from '@/components/dashboard/TagDistributionChart';
|
||||
import { Card, MetricCard } from '@/components/ui';
|
||||
import { DeadlineOverview } from '@/components/deadline/DeadlineOverview';
|
||||
|
||||
interface ProductivityAnalyticsProps {
|
||||
metrics: ProductivityMetrics;
|
||||
deadlineMetrics: DeadlineMetrics;
|
||||
tagMetrics: TagDistributionMetrics;
|
||||
selectedSources: string[];
|
||||
hiddenSources?: string[];
|
||||
}
|
||||
|
||||
export function ProductivityAnalytics({ metrics, deadlineMetrics, selectedSources, hiddenSources = [] }: ProductivityAnalyticsProps) {
|
||||
export function ProductivityAnalytics({ metrics, deadlineMetrics, tagMetrics, selectedSources, hiddenSources = [] }: ProductivityAnalyticsProps) {
|
||||
|
||||
// Filtrer les métriques selon les sources sélectionnées
|
||||
const filteredMetrics = useMemo(() => {
|
||||
@@ -133,6 +136,9 @@ export function ProductivityAnalytics({ metrics, deadlineMetrics, selectedSource
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Distribution par Tags */}
|
||||
<TagDistributionChart metrics={tagMetrics} />
|
||||
|
||||
{/* Insights automatiques */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">💡 Insights</h3>
|
||||
|
||||
204
src/components/dashboard/TagDistributionChart.tsx
Normal file
204
src/components/dashboard/TagDistributionChart.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend, BarChart, Bar, XAxis, YAxis, CartesianGrid, PieLabelRenderProps } from 'recharts';
|
||||
import { TagDistributionMetrics } from '@/services/analytics/tag-analytics';
|
||||
|
||||
interface TagDistributionChartProps {
|
||||
metrics: TagDistributionMetrics;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TagDistributionChart({ metrics, className }: TagDistributionChartProps) {
|
||||
// Préparer les données pour le graphique en camembert
|
||||
const pieData = metrics.tagDistribution.slice(0, 8).map((tag) => ({
|
||||
name: tag.tagName,
|
||||
value: tag.count,
|
||||
percentage: tag.percentage,
|
||||
color: tag.tagColor
|
||||
}));
|
||||
|
||||
// Préparer les données pour le graphique en barres (top tags)
|
||||
const barData = metrics.topTags.slice(0, 10).map(tag => ({
|
||||
name: tag.tagName.length > 12 ? `${tag.tagName.substring(0, 12)}...` : tag.tagName,
|
||||
usage: tag.usage,
|
||||
completionRate: tag.completionRate,
|
||||
avgPriority: tag.avgPriority,
|
||||
color: tag.tagColor
|
||||
}));
|
||||
|
||||
// Tooltip personnalisé pour le camembert
|
||||
const PieTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; value: number; percentage: number } }> }) => {
|
||||
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.value} tâches ({data.percentage.toFixed(1)}%)
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Tooltip personnalisé pour les barres
|
||||
const BarTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; usage: number; completionRate: number } }> }) => {
|
||||
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.usage} tâches
|
||||
</p>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Taux de completion: {data.completionRate.toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Légende personnalisée
|
||||
const CustomLegend = ({ payload }: { payload?: Array<{ value: string; color: string }> }) => {
|
||||
if (!payload) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-sm">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-[var(--foreground)]">{entry.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Distribution par tags - Camembert */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">🏷️ Distribution par Tags</h3>
|
||||
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={(props: PieLabelRenderProps) => {
|
||||
const { name, percent } = props;
|
||||
const percentValue = typeof percent === 'number' ? percent : 0;
|
||||
return percentValue > 0.05 ? `${name}: ${(percentValue * 100).toFixed(1)}%` : '';
|
||||
}}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
>
|
||||
{pieData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<PieTooltip />} />
|
||||
<Legend content={<CustomLegend />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Top tags - Barres */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">📊 Top Tags par Usage</h3>
|
||||
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={barData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 12 }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={80}
|
||||
/>
|
||||
<YAxis tick={{ fontSize: 12 }} />
|
||||
<Tooltip content={<BarTooltip />} />
|
||||
<Bar
|
||||
dataKey="usage"
|
||||
radius={[4, 4, 0, 0]}
|
||||
>
|
||||
{barData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Statistiques des tags */}
|
||||
<Card className="p-6 mt-6">
|
||||
<h3 className="text-lg font-semibold mb-4">📈 Statistiques des Tags</h3>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-[var(--primary)]">
|
||||
{metrics.tagStats.totalTags}
|
||||
</div>
|
||||
<div className="text-sm text-[var(--muted-foreground)]">
|
||||
Tags totaux
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-[var(--success)]">
|
||||
{metrics.tagStats.activeTags}
|
||||
</div>
|
||||
<div className="text-sm text-[var(--muted-foreground)]">
|
||||
Tags actifs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-semibold text-[var(--foreground)] truncate">
|
||||
{metrics.tagStats.mostUsedTag}
|
||||
</div>
|
||||
<div className="text-sm text-[var(--muted-foreground)]">
|
||||
Plus utilisé
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-semibold text-[var(--foreground)] truncate">
|
||||
{metrics.tagStats.leastUsedTag}
|
||||
</div>
|
||||
<div className="text-sm text-[var(--muted-foreground)]">
|
||||
Moins utilisé
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-[var(--accent)]">
|
||||
{metrics.tagStats.avgTasksPerTag.toFixed(1)}
|
||||
</div>
|
||||
<div className="text-sm text-[var(--muted-foreground)]">
|
||||
Moy. tâches/tag
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useTransition } from 'react';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { TagsClient } from '@/clients/tags-client';
|
||||
import { TAG_COLORS } from '@/lib/tag-colors';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -15,20 +16,7 @@ interface TagFormProps {
|
||||
tag?: Tag | null; // Si fourni, mode édition
|
||||
}
|
||||
|
||||
const PRESET_COLORS = [
|
||||
'#3B82F6', // Blue
|
||||
'#EF4444', // Red
|
||||
'#10B981', // Green
|
||||
'#F59E0B', // Yellow
|
||||
'#8B5CF6', // Purple
|
||||
'#EC4899', // Pink
|
||||
'#06B6D4', // Cyan
|
||||
'#84CC16', // Lime
|
||||
'#F97316', // Orange
|
||||
'#6366F1', // Indigo
|
||||
'#14B8A6', // Teal
|
||||
'#F43F5E', // Rose
|
||||
];
|
||||
const PRESET_COLORS = TAG_COLORS;
|
||||
|
||||
export function TagForm({ isOpen, onClose, onSuccess, tag }: TagFormProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
Reference in New Issue
Block a user