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:
274
src/services/analytics/tag-analytics.ts
Normal file
274
src/services/analytics/tag-analytics.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { prisma } from '@/services/core/database';
|
||||
import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
|
||||
import { getToday, subtractDays } from '@/lib/date-utils';
|
||||
import { getTagColor } from '@/lib/tag-colors';
|
||||
|
||||
export interface TagDistributionMetrics {
|
||||
tagDistribution: Array<{
|
||||
tagName: string;
|
||||
tagColor: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
tasks: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
}>;
|
||||
}>;
|
||||
topTags: Array<{
|
||||
tagName: string;
|
||||
tagColor: string;
|
||||
usage: number;
|
||||
completionRate: number;
|
||||
avgPriority: number;
|
||||
}>;
|
||||
tagTrends: Array<{
|
||||
date: string;
|
||||
tagName: string;
|
||||
count: number;
|
||||
}>;
|
||||
tagStats: {
|
||||
totalTags: number;
|
||||
activeTags: number;
|
||||
mostUsedTag: string;
|
||||
leastUsedTag: string;
|
||||
avgTasksPerTag: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}
|
||||
|
||||
export class TagAnalyticsService {
|
||||
/**
|
||||
* Calcule les métriques de distribution par tags
|
||||
*/
|
||||
static async getTagDistributionMetrics(
|
||||
timeRange?: TimeRange,
|
||||
sources?: string[]
|
||||
): Promise<TagDistributionMetrics> {
|
||||
try {
|
||||
const now = getToday();
|
||||
const defaultStart = subtractDays(now, 30); // 30 jours
|
||||
|
||||
const start = timeRange?.start || defaultStart;
|
||||
const end = timeRange?.end || now;
|
||||
|
||||
// Récupérer toutes les tâches avec leurs tags
|
||||
const dbTasks = await prisma.task.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: start,
|
||||
lte: end
|
||||
}
|
||||
},
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Récupérer aussi tous les tags pour avoir leurs couleurs
|
||||
const allTags = await prisma.tag.findMany({
|
||||
select: {
|
||||
name: true,
|
||||
color: true
|
||||
}
|
||||
});
|
||||
|
||||
const tagColorMap = new Map(
|
||||
allTags.map(tag => [tag.name, tag.color])
|
||||
);
|
||||
|
||||
// Convertir en format Task
|
||||
let tasks: Task[] = dbTasks.map(task => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
description: task.description || undefined,
|
||||
status: task.status as TaskStatus,
|
||||
priority: task.priority as TaskPriority,
|
||||
source: task.source as TaskSource,
|
||||
sourceId: task.sourceId || undefined,
|
||||
tags: task.taskTags.map(taskTag => taskTag.tag.name),
|
||||
dueDate: task.dueDate || undefined,
|
||||
completedAt: task.completedAt || undefined,
|
||||
createdAt: task.createdAt,
|
||||
updatedAt: task.updatedAt,
|
||||
jiraProject: task.jiraProject || undefined,
|
||||
jiraKey: task.jiraKey || undefined,
|
||||
jiraType: task.jiraType || undefined,
|
||||
assignee: task.assignee || undefined
|
||||
}));
|
||||
|
||||
// Filtrer par sources si spécifié
|
||||
if (sources && sources.length > 0) {
|
||||
tasks = tasks.filter(task => sources.includes(task.source));
|
||||
}
|
||||
|
||||
return {
|
||||
tagDistribution: this.calculateTagDistribution(tasks, tagColorMap),
|
||||
topTags: this.calculateTopTags(tasks, tagColorMap),
|
||||
tagTrends: this.calculateTagTrends(tasks),
|
||||
tagStats: this.calculateTagStats(tasks)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du calcul des métriques de tags:', error);
|
||||
throw new Error('Impossible de calculer les métriques de distribution par tags');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la distribution des tags
|
||||
*/
|
||||
private static calculateTagDistribution(tasks: Task[], tagColorMap: Map<string, string>) {
|
||||
const tagCounts = new Map<string, { color: string; tasks: Task[] }>();
|
||||
|
||||
// Compter les tâches par tag
|
||||
tasks.forEach(task => {
|
||||
task.tags.forEach(tagName => {
|
||||
if (!tagCounts.has(tagName)) {
|
||||
const tagColor = getTagColor(tagName, tagColorMap.get(tagName));
|
||||
tagCounts.set(tagName, { color: tagColor, tasks: [] });
|
||||
}
|
||||
tagCounts.get(tagName)!.tasks.push(task);
|
||||
});
|
||||
});
|
||||
|
||||
const totalTasks = tasks.length;
|
||||
|
||||
return Array.from(tagCounts.entries())
|
||||
.map(([tagName, data]) => ({
|
||||
tagName,
|
||||
tagColor: data.color,
|
||||
count: data.tasks.length,
|
||||
percentage: totalTasks > 0 ? (data.tasks.length / totalTasks) * 100 : 0,
|
||||
tasks: data.tasks.map(task => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
status: task.status,
|
||||
priority: task.priority
|
||||
}))
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les tags les plus utilisés avec métriques
|
||||
*/
|
||||
private static calculateTopTags(tasks: Task[], tagColorMap: Map<string, string>) {
|
||||
const tagMetrics = new Map<string, {
|
||||
color: string;
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
prioritySum: number;
|
||||
priorityCount: number;
|
||||
}>();
|
||||
|
||||
// Calculer les métriques par tag
|
||||
tasks.forEach(task => {
|
||||
task.tags.forEach(tagName => {
|
||||
if (!tagMetrics.has(tagName)) {
|
||||
const tagColor = getTagColor(tagName, tagColorMap.get(tagName));
|
||||
tagMetrics.set(tagName, {
|
||||
color: tagColor,
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
prioritySum: 0,
|
||||
priorityCount: 0
|
||||
});
|
||||
}
|
||||
|
||||
const metrics = tagMetrics.get(tagName)!;
|
||||
metrics.totalTasks++;
|
||||
|
||||
if (task.status === 'done') {
|
||||
metrics.completedTasks++;
|
||||
}
|
||||
|
||||
// Convertir la priorité en nombre pour le calcul de moyenne
|
||||
const priorityValue = task.priority === 'high' ? 3 :
|
||||
task.priority === 'medium' ? 2 : 1;
|
||||
metrics.prioritySum += priorityValue;
|
||||
metrics.priorityCount++;
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(tagMetrics.entries())
|
||||
.map(([tagName, metrics]) => ({
|
||||
tagName,
|
||||
tagColor: metrics.color,
|
||||
usage: metrics.totalTasks,
|
||||
completionRate: metrics.totalTasks > 0 ?
|
||||
(metrics.completedTasks / metrics.totalTasks) * 100 : 0,
|
||||
avgPriority: metrics.priorityCount > 0 ?
|
||||
metrics.prioritySum / metrics.priorityCount : 0
|
||||
}))
|
||||
.sort((a, b) => b.usage - a.usage)
|
||||
.slice(0, 10); // Top 10
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les tendances des tags dans le temps
|
||||
*/
|
||||
private static calculateTagTrends(tasks: Task[]) {
|
||||
const trends = new Map<string, Map<string, number>>();
|
||||
|
||||
// Grouper par jour et par tag
|
||||
tasks.forEach(task => {
|
||||
const day = task.createdAt.toISOString().split('T')[0];
|
||||
task.tags.forEach(tagName => {
|
||||
if (!trends.has(tagName)) {
|
||||
trends.set(tagName, new Map());
|
||||
}
|
||||
const dayMap = trends.get(tagName)!;
|
||||
dayMap.set(day, (dayMap.get(day) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
// Convertir en format pour le graphique
|
||||
const result: Array<{ date: string; tagName: string; count: number }> = [];
|
||||
trends.forEach((dayMap, tagName) => {
|
||||
dayMap.forEach((count, date) => {
|
||||
result.push({ date, tagName, count });
|
||||
});
|
||||
});
|
||||
|
||||
return result.sort((a, b) => a.date.localeCompare(b.date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les statistiques générales des tags
|
||||
*/
|
||||
private static calculateTagStats(tasks: Task[]) {
|
||||
const allTags = new Set<string>();
|
||||
const tagUsage = new Map<string, number>();
|
||||
|
||||
tasks.forEach(task => {
|
||||
task.tags.forEach(tagName => {
|
||||
allTags.add(tagName);
|
||||
tagUsage.set(tagName, (tagUsage.get(tagName) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
const usageValues = Array.from(tagUsage.values());
|
||||
const mostUsedTag = Array.from(tagUsage.entries())
|
||||
.sort((a, b) => b[1] - a[1])[0]?.[0] || 'Aucun';
|
||||
const leastUsedTag = Array.from(tagUsage.entries())
|
||||
.sort((a, b) => a[1] - b[1])[0]?.[0] || 'Aucun';
|
||||
|
||||
return {
|
||||
totalTags: allTags.size,
|
||||
activeTags: Array.from(tagUsage.values()).filter(count => count > 0).length,
|
||||
mostUsedTag,
|
||||
leastUsedTag,
|
||||
avgTasksPerTag: allTags.size > 0 ?
|
||||
usageValues.reduce((sum, count) => sum + count, 0) / allTags.size : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user