- Added `UserPreferencesProvider` to `RootLayout` for centralized user preferences handling. - Updated components to remove direct user preferences fetching, relying on context instead. - Enhanced SSR data fetching by consolidating user preferences retrieval into a single service call. - Cleaned up unused props in various components to streamline the codebase.
294 lines
8.8 KiB
TypeScript
294 lines
8.8 KiB
TypeScript
import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
|
|
import { prisma } from './database';
|
|
import { getToday, parseDate, subtractDays } from '@/lib/date-utils';
|
|
|
|
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 = 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 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.getTime());
|
|
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 =>
|
|
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 = parseDate(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 = getToday();
|
|
const thisWeekStart = this.getWeekStart(now);
|
|
const lastWeekStart = subtractDays(thisWeekStart, 7);
|
|
const lastWeekEnd = subtractDays(thisWeekStart, 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.getTime());
|
|
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;
|
|
}
|
|
}
|