Files
towercontrol/services/analytics.ts
Julien Froidefond 3c7f5ca2fa 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.
2025-09-18 12:48:06 +02:00

293 lines
8.8 KiB
TypeScript

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