feat: update package dependencies and integrate Recharts
- Changed project name from "towercontrol-temp" to "towercontrol" in package-lock.json and package.json. - Added Recharts library for data visualization in the dashboard. - Updated TODO.md to reflect completion of analytics and metrics integration tasks. - Enhanced RecentTasks component to utilize TaskPriority type for better type safety. - Minor layout adjustments in RecentTasks for improved UI.
This commit is contained in:
292
services/analytics.ts
Normal file
292
services/analytics.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
|
||||
import { prisma } from './database';
|
||||
|
||||
export interface ProductivityMetrics {
|
||||
completionTrend: Array<{
|
||||
date: string;
|
||||
completed: number;
|
||||
created: number;
|
||||
total: number;
|
||||
}>;
|
||||
velocityData: Array<{
|
||||
week: string;
|
||||
completed: number;
|
||||
average: number;
|
||||
}>;
|
||||
priorityDistribution: Array<{
|
||||
priority: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}>;
|
||||
statusFlow: Array<{
|
||||
status: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}>;
|
||||
weeklyStats: {
|
||||
thisWeek: number;
|
||||
lastWeek: number;
|
||||
change: number;
|
||||
changePercent: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}
|
||||
|
||||
export class AnalyticsService {
|
||||
/**
|
||||
* Calcule les métriques de productivité pour une période donnée
|
||||
*/
|
||||
static async getProductivityMetrics(timeRange?: TimeRange): Promise<ProductivityMetrics> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const defaultStart = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 jours
|
||||
|
||||
const start = timeRange?.start || defaultStart;
|
||||
const end = timeRange?.end || now;
|
||||
|
||||
// Récupérer toutes les tâches depuis la base de données avec leurs tags
|
||||
const dbTasks = await prisma.task.findMany({
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convertir en format Task
|
||||
const 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
|
||||
}));
|
||||
|
||||
return {
|
||||
completionTrend: this.calculateCompletionTrend(tasks, start, end),
|
||||
velocityData: this.calculateVelocity(tasks, start, end),
|
||||
priorityDistribution: this.calculatePriorityDistribution(tasks),
|
||||
statusFlow: this.calculateStatusFlow(tasks),
|
||||
weeklyStats: this.calculateWeeklyStats(tasks)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du calcul des métriques:', error);
|
||||
throw new Error('Impossible de calculer les métriques de productivité');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la tendance de completion des tâches par jour
|
||||
*/
|
||||
private static calculateCompletionTrend(tasks: Task[], start: Date, end: Date) {
|
||||
const trend: Array<{ date: string; completed: number; created: number; total: number }> = [];
|
||||
|
||||
// Générer les dates pour la période
|
||||
const currentDate = new Date(start);
|
||||
while (currentDate <= end) {
|
||||
const dateStr = currentDate.toISOString().split('T')[0];
|
||||
|
||||
// Compter les tâches terminées ce jour
|
||||
const completedThisDay = tasks.filter(task =>
|
||||
task.completedAt &&
|
||||
task.completedAt.toISOString().split('T')[0] === dateStr
|
||||
).length;
|
||||
|
||||
// Compter les tâches créées ce jour
|
||||
const createdThisDay = tasks.filter(task =>
|
||||
task.createdAt.toISOString().split('T')[0] === dateStr
|
||||
).length;
|
||||
|
||||
// Total cumulé jusqu'à ce jour
|
||||
const totalUntilThisDay = tasks.filter(task =>
|
||||
new Date(task.createdAt) <= currentDate
|
||||
).length;
|
||||
|
||||
trend.push({
|
||||
date: dateStr,
|
||||
completed: completedThisDay,
|
||||
created: createdThisDay,
|
||||
total: totalUntilThisDay
|
||||
});
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return trend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la vélocité (tâches terminées par semaine)
|
||||
*/
|
||||
private static calculateVelocity(tasks: Task[], start: Date, end: Date) {
|
||||
const weeklyData: Array<{ week: string; completed: number; average: number }> = [];
|
||||
const completedTasks = tasks.filter(task => task.completedAt);
|
||||
|
||||
// Grouper par semaine
|
||||
const weekGroups = new Map<string, number>();
|
||||
|
||||
completedTasks.forEach(task => {
|
||||
if (task.completedAt && task.completedAt >= start && task.completedAt <= end) {
|
||||
const weekStart = this.getWeekStart(task.completedAt);
|
||||
const weekKey = weekStart.toISOString().split('T')[0];
|
||||
weekGroups.set(weekKey, (weekGroups.get(weekKey) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculer la moyenne mobile
|
||||
const values = Array.from(weekGroups.values());
|
||||
const average = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
||||
|
||||
// Convertir en format pour le graphique
|
||||
weekGroups.forEach((count, weekKey) => {
|
||||
const weekDate = new Date(weekKey);
|
||||
weeklyData.push({
|
||||
week: `Sem. ${this.getWeekNumber(weekDate)}`,
|
||||
completed: count,
|
||||
average: Math.round(average * 10) / 10
|
||||
});
|
||||
});
|
||||
|
||||
return weeklyData.sort((a, b) => a.week.localeCompare(b.week));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la distribution des priorités
|
||||
*/
|
||||
private static calculatePriorityDistribution(tasks: Task[]) {
|
||||
const priorityCounts = new Map<string, number>();
|
||||
const total = tasks.length;
|
||||
|
||||
tasks.forEach(task => {
|
||||
const priority = task.priority || 'non-définie';
|
||||
priorityCounts.set(priority, (priorityCounts.get(priority) || 0) + 1);
|
||||
});
|
||||
|
||||
return Array.from(priorityCounts.entries()).map(([priority, count]) => ({
|
||||
priority: this.getPriorityLabel(priority),
|
||||
count,
|
||||
percentage: Math.round((count / total) * 100)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la distribution des statuts
|
||||
*/
|
||||
private static calculateStatusFlow(tasks: Task[]) {
|
||||
const statusCounts = new Map<string, number>();
|
||||
const total = tasks.length;
|
||||
|
||||
tasks.forEach(task => {
|
||||
const status = task.status;
|
||||
statusCounts.set(status, (statusCounts.get(status) || 0) + 1);
|
||||
});
|
||||
|
||||
return Array.from(statusCounts.entries()).map(([status, count]) => ({
|
||||
status: this.getStatusLabel(status),
|
||||
count,
|
||||
percentage: Math.round((count / total) * 100)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les statistiques hebdomadaires
|
||||
*/
|
||||
private static calculateWeeklyStats(tasks: Task[]) {
|
||||
const now = new Date();
|
||||
const thisWeekStart = this.getWeekStart(now);
|
||||
const lastWeekStart = new Date(thisWeekStart.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
const lastWeekEnd = new Date(thisWeekStart.getTime() - 1);
|
||||
|
||||
const thisWeekCompleted = tasks.filter(task =>
|
||||
task.completedAt &&
|
||||
task.completedAt >= thisWeekStart &&
|
||||
task.completedAt <= now
|
||||
).length;
|
||||
|
||||
const lastWeekCompleted = tasks.filter(task =>
|
||||
task.completedAt &&
|
||||
task.completedAt >= lastWeekStart &&
|
||||
task.completedAt <= lastWeekEnd
|
||||
).length;
|
||||
|
||||
const change = thisWeekCompleted - lastWeekCompleted;
|
||||
const changePercent = lastWeekCompleted > 0
|
||||
? Math.round((change / lastWeekCompleted) * 100)
|
||||
: thisWeekCompleted > 0 ? 100 : 0;
|
||||
|
||||
return {
|
||||
thisWeek: thisWeekCompleted,
|
||||
lastWeek: lastWeekCompleted,
|
||||
change,
|
||||
changePercent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient le début de la semaine pour une date
|
||||
*/
|
||||
private static getWeekStart(date: Date): Date {
|
||||
const d = new Date(date);
|
||||
const day = d.getDay();
|
||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Lundi = début de semaine
|
||||
return new Date(d.setDate(diff));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient le numéro de la semaine
|
||||
*/
|
||||
private static getWeekNumber(date: Date): number {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit le code priorité en label français
|
||||
*/
|
||||
private static getPriorityLabel(priority: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
'low': 'Faible',
|
||||
'medium': 'Moyenne',
|
||||
'high': 'Élevée',
|
||||
'urgent': 'Urgente',
|
||||
'non-définie': 'Non définie'
|
||||
};
|
||||
return labels[priority] || priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit le code statut en label français
|
||||
*/
|
||||
private static getStatusLabel(status: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
'backlog': 'Backlog',
|
||||
'todo': 'À faire',
|
||||
'in_progress': 'En cours',
|
||||
'done': 'Terminé',
|
||||
'cancelled': 'Annulé',
|
||||
'freeze': 'Gelé',
|
||||
'archived': 'Archivé'
|
||||
};
|
||||
return labels[status] || status;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user