chore: prettier everywhere
This commit is contained in:
@@ -43,11 +43,14 @@ export class AnalyticsService {
|
||||
/**
|
||||
* Calcule les métriques de productivité pour une période donnée
|
||||
*/
|
||||
static async getProductivityMetrics(timeRange?: TimeRange, sources?: string[]): Promise<ProductivityMetrics> {
|
||||
static async getProductivityMetrics(
|
||||
timeRange?: TimeRange,
|
||||
sources?: string[]
|
||||
): Promise<ProductivityMetrics> {
|
||||
try {
|
||||
const now = getToday();
|
||||
const defaultStart = subtractDays(now, 30); // 30 jours
|
||||
|
||||
|
||||
const start = timeRange?.start || defaultStart;
|
||||
const end = timeRange?.end || now;
|
||||
|
||||
@@ -56,14 +59,14 @@ export class AnalyticsService {
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
}
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Convertir en format Task
|
||||
let tasks: Task[] = dbTasks.map(task => ({
|
||||
let tasks: Task[] = dbTasks.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
description: task.description || undefined,
|
||||
@@ -71,7 +74,7 @@ export class AnalyticsService {
|
||||
priority: task.priority as TaskPriority,
|
||||
source: task.source as TaskSource,
|
||||
sourceId: task.sourceId || undefined,
|
||||
tags: task.taskTags.map(taskTag => taskTag.tag.name),
|
||||
tags: task.taskTags.map((taskTag) => taskTag.tag.name),
|
||||
dueDate: task.dueDate || undefined,
|
||||
completedAt: task.completedAt || undefined,
|
||||
createdAt: task.createdAt,
|
||||
@@ -79,12 +82,12 @@ export class AnalyticsService {
|
||||
jiraProject: task.jiraProject || undefined,
|
||||
jiraKey: task.jiraKey || undefined,
|
||||
jiraType: task.jiraType || undefined,
|
||||
assignee: task.assignee || undefined
|
||||
assignee: task.assignee || undefined,
|
||||
}));
|
||||
|
||||
// Filtrer par sources si spécifié
|
||||
if (sources && sources.length > 0) {
|
||||
tasks = tasks.filter(task => sources.includes(task.source));
|
||||
tasks = tasks.filter((task) => sources.includes(task.source));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -92,7 +95,7 @@ export class AnalyticsService {
|
||||
velocityData: this.calculateVelocity(tasks, start, end),
|
||||
priorityDistribution: this.calculatePriorityDistribution(tasks),
|
||||
statusFlow: this.calculateStatusFlow(tasks),
|
||||
weeklyStats: this.calculateWeeklyStats(tasks)
|
||||
weeklyStats: this.calculateWeeklyStats(tasks),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du calcul des métriques:', error);
|
||||
@@ -103,40 +106,50 @@ export class AnalyticsService {
|
||||
/**
|
||||
* 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 }> = [];
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
const totalUntilThisDay = tasks.filter(
|
||||
(task) => task.createdAt <= currentDate
|
||||
).length;
|
||||
|
||||
trend.push({
|
||||
date: dateStr,
|
||||
completed: completedThisDay,
|
||||
created: createdThisDay,
|
||||
total: totalUntilThisDay
|
||||
total: totalUntilThisDay,
|
||||
});
|
||||
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
|
||||
return trend;
|
||||
}
|
||||
|
||||
@@ -144,28 +157,39 @@ export class AnalyticsService {
|
||||
* 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; weekNumber: number; weekStart: Date }> = [];
|
||||
const completedTasks = tasks.filter(task => task.completedAt);
|
||||
|
||||
const weeklyData: Array<{
|
||||
week: string;
|
||||
completed: number;
|
||||
average: number;
|
||||
weekNumber: number;
|
||||
weekStart: Date;
|
||||
}> = [];
|
||||
const completedTasks = tasks.filter((task) => task.completedAt);
|
||||
|
||||
// Grouper par semaine en utilisant le numéro de semaine comme clé
|
||||
const weekGroups = new Map<number, { count: number; weekStart: Date }>();
|
||||
|
||||
completedTasks.forEach(task => {
|
||||
if (task.completedAt && task.completedAt >= start && task.completedAt <= end) {
|
||||
|
||||
completedTasks.forEach((task) => {
|
||||
if (
|
||||
task.completedAt &&
|
||||
task.completedAt >= start &&
|
||||
task.completedAt <= end
|
||||
) {
|
||||
const weekStart = this.getWeekStart(task.completedAt);
|
||||
const weekNumber = this.getWeekNumber(weekStart);
|
||||
|
||||
|
||||
if (!weekGroups.has(weekNumber)) {
|
||||
weekGroups.set(weekNumber, { count: 0, weekStart });
|
||||
}
|
||||
weekGroups.get(weekNumber)!.count++;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Calculer la moyenne mobile
|
||||
const values = Array.from(weekGroups.values()).map(w => w.count);
|
||||
const average = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
||||
|
||||
const values = Array.from(weekGroups.values()).map((w) => w.count);
|
||||
const average =
|
||||
values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
||||
|
||||
// Convertir en format pour le graphique
|
||||
weekGroups.forEach((data, weekNumber) => {
|
||||
weeklyData.push({
|
||||
@@ -173,10 +197,10 @@ export class AnalyticsService {
|
||||
completed: data.count,
|
||||
average: Math.round(average * 10) / 10,
|
||||
weekNumber,
|
||||
weekStart: data.weekStart
|
||||
weekStart: data.weekStart,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Trier par numéro de semaine (pas alphabétiquement)
|
||||
return weeklyData.sort((a, b) => a.weekNumber - b.weekNumber);
|
||||
}
|
||||
@@ -187,16 +211,16 @@ export class AnalyticsService {
|
||||
private static calculatePriorityDistribution(tasks: Task[]) {
|
||||
const priorityCounts = new Map<string, number>();
|
||||
const total = tasks.length;
|
||||
|
||||
tasks.forEach(task => {
|
||||
|
||||
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)
|
||||
percentage: Math.round((count / total) * 100),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -206,16 +230,16 @@ export class AnalyticsService {
|
||||
private static calculateStatusFlow(tasks: Task[]) {
|
||||
const statusCounts = new Map<string, number>();
|
||||
const total = tasks.length;
|
||||
|
||||
tasks.forEach(task => {
|
||||
|
||||
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)
|
||||
percentage: Math.round((count / total) * 100),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -227,29 +251,34 @@ export class AnalyticsService {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
|
||||
const changePercent =
|
||||
lastWeekCompleted > 0
|
||||
? Math.round((change / lastWeekCompleted) * 100)
|
||||
: thisWeekCompleted > 0
|
||||
? 100
|
||||
: 0;
|
||||
|
||||
return {
|
||||
thisWeek: thisWeekCompleted,
|
||||
lastWeek: lastWeekCompleted,
|
||||
change,
|
||||
changePercent
|
||||
changePercent,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,11 +296,13 @@ export class AnalyticsService {
|
||||
* 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 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);
|
||||
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,11 +310,11 @@ export class AnalyticsService {
|
||||
*/
|
||||
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'
|
||||
low: 'Faible',
|
||||
medium: 'Moyenne',
|
||||
high: 'Élevée',
|
||||
urgent: 'Urgente',
|
||||
'non-définie': 'Non définie',
|
||||
};
|
||||
return labels[priority] || priority;
|
||||
}
|
||||
@@ -293,13 +324,13 @@ export class AnalyticsService {
|
||||
*/
|
||||
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é'
|
||||
backlog: 'Backlog',
|
||||
todo: 'À faire',
|
||||
in_progress: 'En cours',
|
||||
done: 'Terminé',
|
||||
cancelled: 'Annulé',
|
||||
freeze: 'Gelé',
|
||||
archived: 'Archivé',
|
||||
};
|
||||
return labels[status] || status;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface DeadlineTask {
|
||||
export interface DeadlineMetrics {
|
||||
overdue: DeadlineTask[];
|
||||
critical: DeadlineTask[]; // 0-2 jours
|
||||
warning: DeadlineTask[]; // 3-7 jours
|
||||
warning: DeadlineTask[]; // 3-7 jours
|
||||
upcoming: DeadlineTask[]; // 8-14 jours
|
||||
summary: {
|
||||
overdueCount: number;
|
||||
@@ -33,7 +33,9 @@ export class DeadlineAnalyticsService {
|
||||
/**
|
||||
* Analyse les tâches selon leurs échéances
|
||||
*/
|
||||
static async getDeadlineMetrics(sources?: string[]): Promise<DeadlineMetrics> {
|
||||
static async getDeadlineMetrics(
|
||||
sources?: string[]
|
||||
): Promise<DeadlineMetrics> {
|
||||
try {
|
||||
const now = getToday();
|
||||
|
||||
@@ -41,29 +43,31 @@ export class DeadlineAnalyticsService {
|
||||
const dbTasks = await prisma.task.findMany({
|
||||
where: {
|
||||
dueDate: {
|
||||
not: null
|
||||
not: null,
|
||||
},
|
||||
status: {
|
||||
notIn: ['done', 'cancelled', 'archived']
|
||||
}
|
||||
notIn: ['done', 'cancelled', 'archived'],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
dueDate: 'asc'
|
||||
}
|
||||
dueDate: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
// Convertir et analyser les tâches
|
||||
let deadlineTasks: DeadlineTask[] = dbTasks.map(task => {
|
||||
let deadlineTasks: DeadlineTask[] = dbTasks.map((task) => {
|
||||
const dueDate = task.dueDate!;
|
||||
const daysRemaining = Math.ceil((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
const daysRemaining = Math.ceil(
|
||||
(dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
let urgencyLevel: DeadlineTask['urgencyLevel'];
|
||||
if (daysRemaining < 0) {
|
||||
urgencyLevel = 'overdue';
|
||||
@@ -84,25 +88,31 @@ export class DeadlineAnalyticsService {
|
||||
daysRemaining,
|
||||
urgencyLevel,
|
||||
source: task.source,
|
||||
tags: task.taskTags.map(tt => tt.tag.name),
|
||||
jiraKey: task.jiraKey || undefined
|
||||
tags: task.taskTags.map((tt) => tt.tag.name),
|
||||
jiraKey: task.jiraKey || undefined,
|
||||
};
|
||||
});
|
||||
|
||||
// Filtrer par sources si spécifié
|
||||
if (sources && sources.length > 0) {
|
||||
deadlineTasks = deadlineTasks.filter(task => sources.includes(task.source));
|
||||
deadlineTasks = deadlineTasks.filter((task) =>
|
||||
sources.includes(task.source)
|
||||
);
|
||||
}
|
||||
|
||||
// Filtrer les tâches dans les 2 prochaines semaines
|
||||
const relevantTasks = deadlineTasks.filter(task =>
|
||||
task.daysRemaining <= 14 || task.urgencyLevel === 'overdue'
|
||||
const relevantTasks = deadlineTasks.filter(
|
||||
(task) => task.daysRemaining <= 14 || task.urgencyLevel === 'overdue'
|
||||
);
|
||||
|
||||
const overdue = relevantTasks.filter(t => t.urgencyLevel === 'overdue');
|
||||
const critical = relevantTasks.filter(t => t.urgencyLevel === 'critical');
|
||||
const warning = relevantTasks.filter(t => t.urgencyLevel === 'warning');
|
||||
const upcoming = relevantTasks.filter(t => t.urgencyLevel === 'normal' && t.daysRemaining <= 14);
|
||||
const overdue = relevantTasks.filter((t) => t.urgencyLevel === 'overdue');
|
||||
const critical = relevantTasks.filter(
|
||||
(t) => t.urgencyLevel === 'critical'
|
||||
);
|
||||
const warning = relevantTasks.filter((t) => t.urgencyLevel === 'warning');
|
||||
const upcoming = relevantTasks.filter(
|
||||
(t) => t.urgencyLevel === 'normal' && t.daysRemaining <= 14
|
||||
);
|
||||
|
||||
return {
|
||||
overdue,
|
||||
@@ -114,24 +124,23 @@ export class DeadlineAnalyticsService {
|
||||
criticalCount: critical.length,
|
||||
warningCount: warning.length,
|
||||
upcomingCount: upcoming.length,
|
||||
totalWithDeadlines: deadlineTasks.length
|
||||
}
|
||||
totalWithDeadlines: deadlineTasks.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'analyse des échéances:', error);
|
||||
throw new Error('Impossible d\'analyser les échéances');
|
||||
console.error("Erreur lors de l'analyse des échéances:", error);
|
||||
throw new Error("Impossible d'analyser les échéances");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les tâches les plus critiques (en retard + échéance dans 48h)
|
||||
*/
|
||||
static async getCriticalDeadlines(sources?: string[]): Promise<DeadlineTask[]> {
|
||||
static async getCriticalDeadlines(
|
||||
sources?: string[]
|
||||
): Promise<DeadlineTask[]> {
|
||||
const metrics = await this.getDeadlineMetrics(sources);
|
||||
return [
|
||||
...metrics.overdue,
|
||||
...metrics.critical
|
||||
].slice(0, 10); // Limite à 10 tâches les plus critiques
|
||||
return [...metrics.overdue, ...metrics.critical].slice(0, 10); // Limite à 10 tâches les plus critiques
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,8 +153,8 @@ export class DeadlineAnalyticsService {
|
||||
criticalCount: number;
|
||||
}> {
|
||||
const priorityGroups = new Map<string, DeadlineTask[]>();
|
||||
|
||||
tasks.forEach(task => {
|
||||
|
||||
tasks.forEach((task) => {
|
||||
const priority = task.priority || 'medium';
|
||||
if (!priorityGroups.has(priority)) {
|
||||
priorityGroups.set(priority, []);
|
||||
@@ -153,20 +162,30 @@ export class DeadlineAnalyticsService {
|
||||
priorityGroups.get(priority)!.push(task);
|
||||
});
|
||||
|
||||
return Array.from(priorityGroups.entries()).map(([priority, tasks]) => ({
|
||||
priority,
|
||||
count: tasks.length,
|
||||
overdueCount: tasks.filter(t => t.urgencyLevel === 'overdue').length,
|
||||
criticalCount: tasks.filter(t => t.urgencyLevel === 'critical').length
|
||||
})).sort((a, b) => {
|
||||
// Trier par impact (retard + critique) puis par priorité
|
||||
const aImpact = a.overdueCount + a.criticalCount;
|
||||
const bImpact = b.overdueCount + b.criticalCount;
|
||||
if (aImpact !== bImpact) return bImpact - aImpact;
|
||||
|
||||
const priorityOrder: Record<string, number> = { urgent: 4, high: 3, medium: 2, low: 1 };
|
||||
return (priorityOrder[b.priority] || 2) - (priorityOrder[a.priority] || 2);
|
||||
});
|
||||
return Array.from(priorityGroups.entries())
|
||||
.map(([priority, tasks]) => ({
|
||||
priority,
|
||||
count: tasks.length,
|
||||
overdueCount: tasks.filter((t) => t.urgencyLevel === 'overdue').length,
|
||||
criticalCount: tasks.filter((t) => t.urgencyLevel === 'critical')
|
||||
.length,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
// Trier par impact (retard + critique) puis par priorité
|
||||
const aImpact = a.overdueCount + a.criticalCount;
|
||||
const bImpact = b.overdueCount + b.criticalCount;
|
||||
if (aImpact !== bImpact) return bImpact - aImpact;
|
||||
|
||||
const priorityOrder: Record<string, number> = {
|
||||
urgent: 4,
|
||||
high: 3,
|
||||
medium: 2,
|
||||
low: 1,
|
||||
};
|
||||
return (
|
||||
(priorityOrder[b.priority] || 2) - (priorityOrder[a.priority] || 2)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,14 +197,14 @@ export class DeadlineAnalyticsService {
|
||||
recommendation: string;
|
||||
} {
|
||||
const { summary } = metrics;
|
||||
|
||||
|
||||
// Calcul du score de risque basé sur les échéances
|
||||
let riskScore = 0;
|
||||
riskScore += summary.overdueCount * 25; // Retard = très grave
|
||||
riskScore += summary.criticalCount * 15; // Critique = grave
|
||||
riskScore += summary.warningCount * 5; // Avertissement = attention
|
||||
riskScore += summary.upcomingCount * 1; // À venir = surveillance
|
||||
|
||||
riskScore += summary.warningCount * 5; // Avertissement = attention
|
||||
riskScore += summary.upcomingCount * 1; // À venir = surveillance
|
||||
|
||||
// Limiter à 100
|
||||
riskScore = Math.min(riskScore, 100);
|
||||
|
||||
@@ -194,13 +213,16 @@ export class DeadlineAnalyticsService {
|
||||
|
||||
if (riskScore >= 75) {
|
||||
riskLevel = 'critical';
|
||||
recommendation = 'Action immédiate requise ! Plusieurs tâches en retard ou critiques.';
|
||||
recommendation =
|
||||
'Action immédiate requise ! Plusieurs tâches en retard ou critiques.';
|
||||
} else if (riskScore >= 50) {
|
||||
riskLevel = 'high';
|
||||
recommendation = 'Attention : échéances critiques approchent, planifier les priorités.';
|
||||
recommendation =
|
||||
'Attention : échéances critiques approchent, planifier les priorités.';
|
||||
} else if (riskScore >= 25) {
|
||||
riskLevel = 'medium';
|
||||
recommendation = 'Surveillance nécessaire, quelques échéances à surveiller.';
|
||||
recommendation =
|
||||
'Surveillance nécessaire, quelques échéances à surveiller.';
|
||||
} else {
|
||||
riskLevel = 'low';
|
||||
recommendation = 'Situation stable, échéances sous contrôle.';
|
||||
|
||||
@@ -12,7 +12,7 @@ type TaskType = {
|
||||
taskTags?: {
|
||||
tag: {
|
||||
name: string;
|
||||
}
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ type CheckboxType = {
|
||||
taskTags?: {
|
||||
tag: {
|
||||
name: string;
|
||||
}
|
||||
};
|
||||
}[];
|
||||
} | null;
|
||||
};
|
||||
@@ -86,36 +86,44 @@ export class ManagerSummaryService {
|
||||
/**
|
||||
* Génère un résumé orienté manager pour les 7 derniers jours
|
||||
*/
|
||||
static async getManagerSummary(date: Date = getToday()): Promise<ManagerSummary> {
|
||||
static async getManagerSummary(
|
||||
date: Date = getToday()
|
||||
): Promise<ManagerSummary> {
|
||||
// Fenêtre glissante de 7 jours au lieu de semaine calendaire
|
||||
const weekEnd = new Date(date);
|
||||
const weekStart = new Date(date);
|
||||
weekStart.setDate(weekStart.getDate() - 6); // 7 jours en arrière (incluant aujourd'hui)
|
||||
|
||||
|
||||
// Récupérer les données de base
|
||||
const [tasks, checkboxes] = await Promise.all([
|
||||
this.getCompletedTasks(weekStart, weekEnd),
|
||||
this.getCompletedCheckboxes(weekStart, weekEnd)
|
||||
this.getCompletedCheckboxes(weekStart, weekEnd),
|
||||
]);
|
||||
|
||||
// Analyser et extraire les accomplissements clés
|
||||
const keyAccomplishments = await this.extractKeyAccomplishments(tasks, checkboxes);
|
||||
|
||||
const keyAccomplishments = await this.extractKeyAccomplishments(
|
||||
tasks,
|
||||
checkboxes
|
||||
);
|
||||
|
||||
// Identifier les défis à venir
|
||||
const upcomingChallenges = await this.identifyUpcomingChallenges();
|
||||
|
||||
|
||||
// Calculer les métriques
|
||||
const metrics = this.calculateMetrics(tasks, checkboxes);
|
||||
|
||||
|
||||
// Générer le narratif
|
||||
const narrative = this.generateNarrative(keyAccomplishments, upcomingChallenges);
|
||||
const narrative = this.generateNarrative(
|
||||
keyAccomplishments,
|
||||
upcomingChallenges
|
||||
);
|
||||
|
||||
return {
|
||||
period: { start: weekStart, end: weekEnd },
|
||||
keyAccomplishments,
|
||||
upcomingChallenges,
|
||||
metrics,
|
||||
narrative
|
||||
narrative,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -130,13 +138,13 @@ export class ManagerSummaryService {
|
||||
{
|
||||
completedAt: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
}
|
||||
}
|
||||
]
|
||||
lte: endDate,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
completedAt: 'desc'
|
||||
completedAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -150,14 +158,14 @@ export class ManagerSummaryService {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
@@ -170,8 +178,8 @@ export class ManagerSummaryService {
|
||||
isChecked: true,
|
||||
date: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
}
|
||||
lte: endDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -189,77 +197,80 @@ export class ManagerSummaryService {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc'
|
||||
}
|
||||
date: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return checkboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait les accomplissements clés basés sur la priorité
|
||||
*/
|
||||
private static async extractKeyAccomplishments(tasks: TaskType[], checkboxes: CheckboxType[]): Promise<KeyAccomplishment[]> {
|
||||
private static async extractKeyAccomplishments(
|
||||
tasks: TaskType[],
|
||||
checkboxes: CheckboxType[]
|
||||
): Promise<KeyAccomplishment[]> {
|
||||
const accomplishments: KeyAccomplishment[] = [];
|
||||
|
||||
|
||||
// Tâches: prendre toutes les high/medium priority, et quelques low si significatives
|
||||
for (const task of tasks) {
|
||||
const priority = task.priority.toLowerCase();
|
||||
|
||||
|
||||
// Convertir priorité task en impact accomplissement
|
||||
let impact: 'high' | 'medium' | 'low';
|
||||
if (priority === 'high' || priority === 'urgent') {
|
||||
impact = 'high';
|
||||
} else if (priority === 'medium') {
|
||||
impact = 'medium';
|
||||
impact = 'medium';
|
||||
} else {
|
||||
// Pour les low priority, tout prendre
|
||||
impact = 'low';
|
||||
}
|
||||
|
||||
|
||||
// Compter TOUS les todos associés à cette tâche (pas seulement ceux de la période)
|
||||
// car l'accomplissement c'est la tâche complétée, pas seulement les todos de la période
|
||||
const allRelatedTodos = await prisma.dailyCheckbox.count({
|
||||
where: {
|
||||
task: {
|
||||
id: task.id
|
||||
}
|
||||
}
|
||||
id: task.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
accomplishments.push({
|
||||
id: `task-${task.id}`,
|
||||
title: task.title,
|
||||
description: task.description || undefined,
|
||||
tags: task.taskTags?.map(tt => tt.tag.name) || [],
|
||||
tags: task.taskTags?.map((tt) => tt.tag.name) || [],
|
||||
impact,
|
||||
completedAt: task.completedAt || task.updatedAt,
|
||||
updatedAt: task.updatedAt,
|
||||
relatedItems: [task.id],
|
||||
todosCount: allRelatedTodos // Nombre total de todos associés à cette tâche
|
||||
todosCount: allRelatedTodos, // Nombre total de todos associés à cette tâche
|
||||
});
|
||||
}
|
||||
|
||||
// AJOUTER les todos standalone avec la nouvelle règle de priorité
|
||||
// Exclure les todos déjà comptés dans les tâches complétées
|
||||
const standaloneTodos = checkboxes.filter(checkbox =>
|
||||
!checkbox.task // Todos non liés à une tâche
|
||||
const standaloneTodos = checkboxes.filter(
|
||||
(checkbox) => !checkbox.task // Todos non liés à une tâche
|
||||
);
|
||||
|
||||
standaloneTodos.forEach(todo => {
|
||||
|
||||
standaloneTodos.forEach((todo) => {
|
||||
// Appliquer la nouvelle règle de priorité :
|
||||
// Si pas de tâche associée, priorité faible (même pour les meetings)
|
||||
const impact: 'high' | 'medium' | 'low' = 'low';
|
||||
|
||||
|
||||
accomplishments.push({
|
||||
id: `todo-${todo.id}`,
|
||||
title: todo.type === 'meeting' ? `📅 ${todo.text}` : todo.text,
|
||||
@@ -268,35 +279,35 @@ export class ManagerSummaryService {
|
||||
completedAt: todo.date,
|
||||
updatedAt: todo.date, // Pour les todos, updatedAt = completedAt
|
||||
relatedItems: [todo.id],
|
||||
todosCount: 1 // Un todo = 1 todo
|
||||
todosCount: 1, // Un todo = 1 todo
|
||||
});
|
||||
});
|
||||
|
||||
// Trier par impact puis par date
|
||||
return accomplishments
|
||||
.sort((a, b) => {
|
||||
const impactOrder = { high: 3, medium: 2, low: 1 };
|
||||
if (impactOrder[a.impact] !== impactOrder[b.impact]) {
|
||||
return impactOrder[b.impact] - impactOrder[a.impact];
|
||||
}
|
||||
return b.completedAt.getTime() - a.completedAt.getTime();
|
||||
})
|
||||
// Pas de limite - afficher tous les accomplissements
|
||||
return accomplishments.sort((a, b) => {
|
||||
const impactOrder = { high: 3, medium: 2, low: 1 };
|
||||
if (impactOrder[a.impact] !== impactOrder[b.impact]) {
|
||||
return impactOrder[b.impact] - impactOrder[a.impact];
|
||||
}
|
||||
return b.completedAt.getTime() - a.completedAt.getTime();
|
||||
});
|
||||
// Pas de limite - afficher tous les accomplissements
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifie les défis et enjeux à venir
|
||||
*/
|
||||
private static async identifyUpcomingChallenges(): Promise<UpcomingChallenge[]> {
|
||||
|
||||
private static async identifyUpcomingChallenges(): Promise<
|
||||
UpcomingChallenge[]
|
||||
> {
|
||||
// Récupérer les tâches à venir (priorité high/medium en premier)
|
||||
const upcomingTasks = await prisma.task.findMany({
|
||||
where: {
|
||||
completedAt: null
|
||||
completedAt: null,
|
||||
},
|
||||
orderBy: [
|
||||
{ priority: 'asc' }, // high < medium < low
|
||||
{ createdAt: 'desc' }
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
select: {
|
||||
id: true,
|
||||
@@ -308,13 +319,13 @@ export class ManagerSummaryService {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 30
|
||||
take: 30,
|
||||
});
|
||||
|
||||
// Récupérer les checkboxes récurrentes non complétées (meetings + tâches prioritaires)
|
||||
@@ -322,18 +333,18 @@ export class ManagerSummaryService {
|
||||
where: {
|
||||
isChecked: false,
|
||||
date: {
|
||||
gte: getToday()
|
||||
gte: getToday(),
|
||||
},
|
||||
OR: [
|
||||
{ type: 'meeting' },
|
||||
{
|
||||
{
|
||||
task: {
|
||||
priority: {
|
||||
in: ['high', 'medium']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
in: ['high', 'medium'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -351,27 +362,24 @@ export class ManagerSummaryService {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ date: 'asc' },
|
||||
{ createdAt: 'asc' }
|
||||
],
|
||||
take: 20
|
||||
orderBy: [{ date: 'asc' }, { createdAt: 'asc' }],
|
||||
take: 20,
|
||||
});
|
||||
|
||||
const challenges: UpcomingChallenge[] = [];
|
||||
|
||||
|
||||
// Analyser les tâches - se baser sur la priorité réelle
|
||||
upcomingTasks.forEach((task) => {
|
||||
const taskPriority = task.priority.toLowerCase();
|
||||
|
||||
|
||||
// Convertir priorité task en priorité challenge
|
||||
let priority: 'high' | 'medium' | 'low';
|
||||
if (taskPriority === 'high' || taskPriority === 'urgent') {
|
||||
@@ -382,32 +390,40 @@ export class ManagerSummaryService {
|
||||
// Pour les low priority, tout prendre
|
||||
priority = 'low';
|
||||
}
|
||||
|
||||
const estimatedEffort = this.estimateEffort(task.title, task.description || undefined);
|
||||
|
||||
|
||||
const estimatedEffort = this.estimateEffort(
|
||||
task.title,
|
||||
task.description || undefined
|
||||
);
|
||||
|
||||
// Compter les todos associés à cette tâche
|
||||
const relatedTodos = upcomingCheckboxes.filter(cb => cb.task?.id === task.id);
|
||||
|
||||
const relatedTodos = upcomingCheckboxes.filter(
|
||||
(cb) => cb.task?.id === task.id
|
||||
);
|
||||
|
||||
challenges.push({
|
||||
id: `task-${task.id}`,
|
||||
title: task.title,
|
||||
description: task.description || undefined,
|
||||
tags: task.taskTags?.map(tt => tt.tag.name) || [],
|
||||
tags: task.taskTags?.map((tt) => tt.tag.name) || [],
|
||||
priority,
|
||||
estimatedEffort,
|
||||
blockers: this.identifyBlockers(task.title, task.description || undefined),
|
||||
relatedItems: [task.id, ...relatedTodos.map(t => t.id)],
|
||||
todosCount: relatedTodos.length // Nombre réel de todos associés
|
||||
blockers: this.identifyBlockers(
|
||||
task.title,
|
||||
task.description || undefined
|
||||
),
|
||||
relatedItems: [task.id, ...relatedTodos.map((t) => t.id)],
|
||||
todosCount: relatedTodos.length, // Nombre réel de todos associés
|
||||
});
|
||||
});
|
||||
|
||||
// Ajouter les todos importants comme challenges
|
||||
upcomingCheckboxes.forEach(checkbox => {
|
||||
upcomingCheckboxes.forEach((checkbox) => {
|
||||
// Déterminer la priorité selon la nouvelle règle :
|
||||
// Si le todo est associé à une tâche, prendre la priorité de la tâche
|
||||
// Sinon, priorité faible par défaut (même pour les meetings)
|
||||
let priority: 'high' | 'medium' | 'low';
|
||||
|
||||
|
||||
if (checkbox.task?.priority) {
|
||||
const taskPriority = checkbox.task.priority.toLowerCase();
|
||||
if (taskPriority === 'high') {
|
||||
@@ -427,33 +443,43 @@ export class ManagerSummaryService {
|
||||
challenges.push({
|
||||
id: `checkbox-${checkbox.id}`,
|
||||
title: checkbox.text,
|
||||
tags: checkbox.task?.taskTags?.map(tt => tt.tag.name) || [],
|
||||
tags: checkbox.task?.taskTags?.map((tt) => tt.tag.name) || [],
|
||||
priority,
|
||||
estimatedEffort: 'hours',
|
||||
blockers: [],
|
||||
relatedItems: [checkbox.id],
|
||||
todosCount: 1 // Une checkbox = 1 todo
|
||||
todosCount: 1, // Une checkbox = 1 todo
|
||||
});
|
||||
});
|
||||
|
||||
return challenges
|
||||
.sort((a, b) => {
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
})
|
||||
// Pas de limite - afficher tous les challenges
|
||||
return challenges.sort((a, b) => {
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
});
|
||||
// Pas de limite - afficher tous les challenges
|
||||
}
|
||||
|
||||
/**
|
||||
* Estime l'effort requis
|
||||
*/
|
||||
private static estimateEffort(title: string, description?: string): 'days' | 'weeks' | 'hours' {
|
||||
private static estimateEffort(
|
||||
title: string,
|
||||
description?: string
|
||||
): 'days' | 'weeks' | 'hours' {
|
||||
const content = `${title} ${description || ''}`.toLowerCase();
|
||||
|
||||
if (content.includes('architecture') || content.includes('migration') || content.includes('refactor')) {
|
||||
|
||||
if (
|
||||
content.includes('architecture') ||
|
||||
content.includes('migration') ||
|
||||
content.includes('refactor')
|
||||
) {
|
||||
return 'weeks';
|
||||
}
|
||||
if (content.includes('feature') || content.includes('implement') || content.includes('integration')) {
|
||||
if (
|
||||
content.includes('feature') ||
|
||||
content.includes('implement') ||
|
||||
content.includes('integration')
|
||||
) {
|
||||
return 'days';
|
||||
}
|
||||
return 'hours';
|
||||
@@ -462,10 +488,13 @@ export class ManagerSummaryService {
|
||||
/**
|
||||
* Identifie les blockers potentiels
|
||||
*/
|
||||
private static identifyBlockers(title: string, description?: string): string[] {
|
||||
private static identifyBlockers(
|
||||
title: string,
|
||||
description?: string
|
||||
): string[] {
|
||||
const content = `${title} ${description || ''}`.toLowerCase();
|
||||
const blockers: string[] = [];
|
||||
|
||||
|
||||
if (content.includes('depends') || content.includes('waiting')) {
|
||||
blockers.push('Dépendances externes');
|
||||
}
|
||||
@@ -475,22 +504,28 @@ export class ManagerSummaryService {
|
||||
if (content.includes('design') && !content.includes('implement')) {
|
||||
blockers.push('Spécifications incomplètes');
|
||||
}
|
||||
|
||||
|
||||
return blockers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calcule les métriques résumées
|
||||
*/
|
||||
private static calculateMetrics(tasks: TaskType[], checkboxes: CheckboxType[]) {
|
||||
private static calculateMetrics(
|
||||
tasks: TaskType[],
|
||||
checkboxes: CheckboxType[]
|
||||
) {
|
||||
const totalTasksCompleted = tasks.length;
|
||||
const totalCheckboxesCompleted = checkboxes.length;
|
||||
|
||||
|
||||
// Calculer les métriques détaillées
|
||||
const highPriorityTasksCompleted = tasks.filter(t => t.priority.toLowerCase() === 'high').length;
|
||||
const meetingCheckboxesCompleted = checkboxes.filter(c => c.type === 'meeting').length;
|
||||
|
||||
const highPriorityTasksCompleted = tasks.filter(
|
||||
(t) => t.priority.toLowerCase() === 'high'
|
||||
).length;
|
||||
const meetingCheckboxesCompleted = checkboxes.filter(
|
||||
(c) => c.type === 'meeting'
|
||||
).length;
|
||||
|
||||
// Analyser la répartition par catégorie
|
||||
const focusAreas: { [category: string]: number } = {};
|
||||
|
||||
@@ -500,7 +535,7 @@ export class ManagerSummaryService {
|
||||
highPriorityTasksCompleted,
|
||||
meetingCheckboxesCompleted,
|
||||
completionRate: 0, // À calculer par rapport aux objectifs
|
||||
focusAreas
|
||||
focusAreas,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -508,31 +543,34 @@ export class ManagerSummaryService {
|
||||
* Génère le narratif pour le manager
|
||||
*/
|
||||
private static generateNarrative(
|
||||
accomplishments: KeyAccomplishment[],
|
||||
accomplishments: KeyAccomplishment[],
|
||||
challenges: UpcomingChallenge[]
|
||||
) {
|
||||
// Points forts des 7 derniers jours
|
||||
const topAccomplishments = accomplishments.slice(0, 3);
|
||||
const weekHighlight = topAccomplishments.length > 0
|
||||
? `Ces 7 derniers jours, j'ai principalement progressé sur ${topAccomplishments.map(a => a.title).join(', ')}.`
|
||||
: 'Période focalisée sur l\'exécution des tâches quotidiennes.';
|
||||
const weekHighlight =
|
||||
topAccomplishments.length > 0
|
||||
? `Ces 7 derniers jours, j'ai principalement progressé sur ${topAccomplishments.map((a) => a.title).join(', ')}.`
|
||||
: "Période focalisée sur l'exécution des tâches quotidiennes.";
|
||||
|
||||
// Défis rencontrés
|
||||
const highImpactItems = accomplishments.filter(a => a.impact === 'high');
|
||||
const mainChallenges = highImpactItems.length > 0
|
||||
? `Les principaux enjeux traités ont été liés aux ${[...new Set(highImpactItems.flatMap(a => a.tags))].join(', ')}.`
|
||||
: 'Pas de blockers majeurs rencontrés sur cette période.';
|
||||
const highImpactItems = accomplishments.filter((a) => a.impact === 'high');
|
||||
const mainChallenges =
|
||||
highImpactItems.length > 0
|
||||
? `Les principaux enjeux traités ont été liés aux ${[...new Set(highImpactItems.flatMap((a) => a.tags))].join(', ')}.`
|
||||
: 'Pas de blockers majeurs rencontrés sur cette période.';
|
||||
|
||||
// Focus 7 prochains jours
|
||||
const topChallenges = challenges.slice(0, 3);
|
||||
const nextWeekFocus = topChallenges.length > 0
|
||||
? `Les 7 prochains jours seront concentrés sur ${topChallenges.map(c => c.title).join(', ')}.`
|
||||
: 'Continuation du travail en cours selon les priorités établies.';
|
||||
const nextWeekFocus =
|
||||
topChallenges.length > 0
|
||||
? `Les 7 prochains jours seront concentrés sur ${topChallenges.map((c) => c.title).join(', ')}.`
|
||||
: 'Continuation du travail en cours selon les priorités établies.';
|
||||
|
||||
return {
|
||||
weekHighlight,
|
||||
mainChallenges,
|
||||
nextWeekFocus
|
||||
nextWeekFocus,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { prisma } from '@/services/core/database';
|
||||
import { eachDayOfInterval, format, startOfDay, endOfDay, startOfWeek, endOfWeek } from 'date-fns';
|
||||
import {
|
||||
eachDayOfInterval,
|
||||
format,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
} from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { formatDateForAPI, getDayName, getToday, subtractDays } from '@/lib/date-utils';
|
||||
import {
|
||||
formatDateForAPI,
|
||||
getDayName,
|
||||
getToday,
|
||||
subtractDays,
|
||||
} from '@/lib/date-utils';
|
||||
|
||||
export interface DailyMetrics {
|
||||
date: string; // Format ISO
|
||||
@@ -59,35 +71,43 @@ export class MetricsService {
|
||||
/**
|
||||
* Récupère les métriques journalières des 7 derniers jours
|
||||
*/
|
||||
static async getWeeklyMetrics(date: Date = getToday()): Promise<WeeklyMetricsOverview> {
|
||||
static async getWeeklyMetrics(
|
||||
date: Date = getToday()
|
||||
): Promise<WeeklyMetricsOverview> {
|
||||
// Fenêtre glissante de 7 jours au lieu de semaine calendaire
|
||||
const weekEnd = new Date(date);
|
||||
const weekStart = new Date(date);
|
||||
weekStart.setDate(weekStart.getDate() - 6); // 7 jours en arrière (incluant aujourd'hui)
|
||||
|
||||
|
||||
// Générer tous les jours de la semaine
|
||||
const daysOfWeek = eachDayOfInterval({ start: weekStart, end: weekEnd });
|
||||
|
||||
|
||||
// Récupérer les données pour chaque jour
|
||||
const dailyBreakdown = await Promise.all(
|
||||
daysOfWeek.map(day => this.getDailyMetrics(day))
|
||||
daysOfWeek.map((day) => this.getDailyMetrics(day))
|
||||
);
|
||||
|
||||
// Calculer les métriques de résumé
|
||||
const summary = this.calculateWeeklySummary(dailyBreakdown);
|
||||
|
||||
|
||||
// Récupérer la distribution des statuts pour la semaine
|
||||
const statusDistribution = await this.getStatusDistribution(weekStart, weekEnd);
|
||||
|
||||
const statusDistribution = await this.getStatusDistribution(
|
||||
weekStart,
|
||||
weekEnd
|
||||
);
|
||||
|
||||
// Récupérer la répartition par priorité
|
||||
const priorityBreakdown = await this.getPriorityBreakdown(weekStart, weekEnd);
|
||||
const priorityBreakdown = await this.getPriorityBreakdown(
|
||||
weekStart,
|
||||
weekEnd
|
||||
);
|
||||
|
||||
return {
|
||||
period: { start: weekStart, end: weekEnd },
|
||||
dailyBreakdown,
|
||||
summary,
|
||||
statusDistribution,
|
||||
priorityBreakdown
|
||||
priorityBreakdown,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,71 +117,72 @@ export class MetricsService {
|
||||
private static async getDailyMetrics(date: Date): Promise<DailyMetrics> {
|
||||
const dayStart = startOfDay(date);
|
||||
const dayEnd = endOfDay(date);
|
||||
|
||||
|
||||
// Compter les tâches par statut à la fin de la journée
|
||||
const [completed, inProgress, blocked, pending, newTasks, totalTasks] = await Promise.all([
|
||||
// Tâches complétées ce jour
|
||||
prisma.task.count({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
completedAt: {
|
||||
gte: dayStart,
|
||||
lte: dayEnd
|
||||
}
|
||||
const [completed, inProgress, blocked, pending, newTasks, totalTasks] =
|
||||
await Promise.all([
|
||||
// Tâches complétées ce jour
|
||||
prisma.task.count({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
completedAt: {
|
||||
gte: dayStart,
|
||||
lte: dayEnd,
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 'done',
|
||||
updatedAt: {
|
||||
gte: dayStart,
|
||||
lte: dayEnd,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
// Tâches en cours (status = in_progress à ce moment)
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: 'in_progress',
|
||||
createdAt: { lte: dayEnd },
|
||||
},
|
||||
}),
|
||||
|
||||
// Tâches bloquées
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: 'blocked',
|
||||
createdAt: { lte: dayEnd },
|
||||
},
|
||||
}),
|
||||
|
||||
// Tâches en attente
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: 'pending',
|
||||
createdAt: { lte: dayEnd },
|
||||
},
|
||||
}),
|
||||
|
||||
// Nouvelles tâches créées ce jour
|
||||
prisma.task.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: dayStart,
|
||||
lte: dayEnd,
|
||||
},
|
||||
{
|
||||
status: 'done',
|
||||
updatedAt: {
|
||||
gte: dayStart,
|
||||
lte: dayEnd
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
// Tâches en cours (status = in_progress à ce moment)
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: 'in_progress',
|
||||
createdAt: { lte: dayEnd }
|
||||
}
|
||||
}),
|
||||
|
||||
// Tâches bloquées
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: 'blocked',
|
||||
createdAt: { lte: dayEnd }
|
||||
}
|
||||
}),
|
||||
|
||||
// Tâches en attente
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: 'pending',
|
||||
createdAt: { lte: dayEnd }
|
||||
}
|
||||
}),
|
||||
|
||||
// Nouvelles tâches créées ce jour
|
||||
prisma.task.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: dayStart,
|
||||
lte: dayEnd
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// Total des tâches existantes ce jour
|
||||
prisma.task.count({
|
||||
where: {
|
||||
createdAt: { lte: dayEnd }
|
||||
}
|
||||
})
|
||||
]);
|
||||
},
|
||||
}),
|
||||
|
||||
// Total des tâches existantes ce jour
|
||||
prisma.task.count({
|
||||
where: {
|
||||
createdAt: { lte: dayEnd },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const completionRate = totalTasks > 0 ? (completed / totalTasks) * 100 : 0;
|
||||
|
||||
@@ -174,7 +195,7 @@ export class MetricsService {
|
||||
pending,
|
||||
newTasks,
|
||||
totalTasks,
|
||||
completionRate: Math.round(completionRate * 100) / 100
|
||||
completionRate: Math.round(completionRate * 100) / 100,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,24 +203,35 @@ export class MetricsService {
|
||||
* Calcule le résumé hebdomadaire
|
||||
*/
|
||||
private static calculateWeeklySummary(dailyBreakdown: DailyMetrics[]) {
|
||||
const totalTasksCompleted = dailyBreakdown.reduce((sum, day) => sum + day.completed, 0);
|
||||
const totalTasksCreated = dailyBreakdown.reduce((sum, day) => sum + day.newTasks, 0);
|
||||
const averageCompletionRate = dailyBreakdown.reduce((sum, day) => sum + day.completionRate, 0) / dailyBreakdown.length;
|
||||
|
||||
const totalTasksCompleted = dailyBreakdown.reduce(
|
||||
(sum, day) => sum + day.completed,
|
||||
0
|
||||
);
|
||||
const totalTasksCreated = dailyBreakdown.reduce(
|
||||
(sum, day) => sum + day.newTasks,
|
||||
0
|
||||
);
|
||||
const averageCompletionRate =
|
||||
dailyBreakdown.reduce((sum, day) => sum + day.completionRate, 0) /
|
||||
dailyBreakdown.length;
|
||||
|
||||
// Identifier les jours de pic et de creux
|
||||
const peakDay = dailyBreakdown.reduce((peak, day) =>
|
||||
const peakDay = dailyBreakdown.reduce((peak, day) =>
|
||||
day.completed > peak.completed ? day : peak
|
||||
);
|
||||
const lowDay = dailyBreakdown.reduce((low, day) =>
|
||||
const lowDay = dailyBreakdown.reduce((low, day) =>
|
||||
day.completed < low.completed ? day : low
|
||||
);
|
||||
|
||||
// Analyser les tendances
|
||||
const firstHalf = dailyBreakdown.slice(0, 3);
|
||||
const secondHalf = dailyBreakdown.slice(4);
|
||||
const firstHalfAvg = firstHalf.reduce((sum, day) => sum + day.completed, 0) / firstHalf.length;
|
||||
const secondHalfAvg = secondHalf.reduce((sum, day) => sum + day.completed, 0) / secondHalf.length;
|
||||
|
||||
const firstHalfAvg =
|
||||
firstHalf.reduce((sum, day) => sum + day.completed, 0) / firstHalf.length;
|
||||
const secondHalfAvg =
|
||||
secondHalf.reduce((sum, day) => sum + day.completed, 0) /
|
||||
secondHalf.length;
|
||||
|
||||
let completionTrend: 'improving' | 'declining' | 'stable';
|
||||
if (secondHalfAvg > firstHalfAvg * 1.1) {
|
||||
completionTrend = 'improving';
|
||||
@@ -212,17 +244,23 @@ export class MetricsService {
|
||||
// Analyser le pattern de productivité
|
||||
const weekendDays = dailyBreakdown.slice(5); // Samedi et dimanche
|
||||
const weekdayDays = dailyBreakdown.slice(0, 5);
|
||||
const weekendAvg = weekendDays.reduce((sum, day) => sum + day.completed, 0) / weekendDays.length;
|
||||
const weekdayAvg = weekdayDays.reduce((sum, day) => sum + day.completed, 0) / weekdayDays.length;
|
||||
|
||||
const weekendAvg =
|
||||
weekendDays.reduce((sum, day) => sum + day.completed, 0) /
|
||||
weekendDays.length;
|
||||
const weekdayAvg =
|
||||
weekdayDays.reduce((sum, day) => sum + day.completed, 0) /
|
||||
weekdayDays.length;
|
||||
|
||||
let productivityPattern: 'consistent' | 'variable' | 'weekend-heavy';
|
||||
if (weekendAvg > weekdayAvg * 1.2) {
|
||||
productivityPattern = 'weekend-heavy';
|
||||
} else {
|
||||
const variance = dailyBreakdown.reduce((sum, day) => {
|
||||
const diff = day.completed - (totalTasksCompleted / dailyBreakdown.length);
|
||||
return sum + diff * diff;
|
||||
}, 0) / dailyBreakdown.length;
|
||||
const variance =
|
||||
dailyBreakdown.reduce((sum, day) => {
|
||||
const diff =
|
||||
day.completed - totalTasksCompleted / dailyBreakdown.length;
|
||||
return sum + diff * diff;
|
||||
}, 0) / dailyBreakdown.length;
|
||||
productivityPattern = variance > 4 ? 'variable' : 'consistent';
|
||||
}
|
||||
|
||||
@@ -234,8 +272,8 @@ export class MetricsService {
|
||||
lowProductivityDay: lowDay.dayName,
|
||||
trendsAnalysis: {
|
||||
completionTrend,
|
||||
productivityPattern
|
||||
}
|
||||
productivityPattern,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -246,31 +284,34 @@ export class MetricsService {
|
||||
const statusCounts = await prisma.task.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
status: true
|
||||
status: true,
|
||||
},
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: start,
|
||||
lte: end
|
||||
}
|
||||
}
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const total = statusCounts.reduce((sum, item) => sum + item._count.status, 0);
|
||||
|
||||
const total = statusCounts.reduce(
|
||||
(sum, item) => sum + item._count.status,
|
||||
0
|
||||
);
|
||||
|
||||
const statusColors: { [key: string]: string } = {
|
||||
pending: '#94a3b8', // gray
|
||||
in_progress: '#3b82f6', // blue
|
||||
blocked: '#ef4444', // red
|
||||
done: '#10b981', // green
|
||||
archived: '#6b7280' // gray-500
|
||||
archived: '#6b7280', // gray-500
|
||||
};
|
||||
|
||||
return statusCounts.map(item => ({
|
||||
return statusCounts.map((item) => ({
|
||||
status: item.status,
|
||||
count: item._count.status,
|
||||
percentage: Math.round((item._count.status / total) * 100 * 100) / 100,
|
||||
color: statusColors[item.status] || '#6b7280'
|
||||
color: statusColors[item.status] || '#6b7280',
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -279,7 +320,7 @@ export class MetricsService {
|
||||
*/
|
||||
private static async getPriorityBreakdown(start: Date, end: Date) {
|
||||
const priorities = ['high', 'medium', 'low'];
|
||||
|
||||
|
||||
const priorityData = await Promise.all(
|
||||
priorities.map(async (priority) => {
|
||||
const [completed] = await Promise.all([
|
||||
@@ -289,10 +330,10 @@ export class MetricsService {
|
||||
priority,
|
||||
completedAt: {
|
||||
gte: start,
|
||||
lte: end
|
||||
}
|
||||
}
|
||||
})
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Calculer les tâches en cours de cette priorité
|
||||
@@ -300,9 +341,9 @@ export class MetricsService {
|
||||
where: {
|
||||
priority,
|
||||
status: {
|
||||
notIn: ['done', 'cancelled', 'archived']
|
||||
}
|
||||
}
|
||||
notIn: ['done', 'cancelled', 'archived'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Le total pour le calcul de completion = complétées + en cours
|
||||
@@ -317,8 +358,12 @@ export class MetricsService {
|
||||
pending,
|
||||
total,
|
||||
completionRate: Math.round(completionRate * 100) / 100,
|
||||
color: priority === 'high' ? '#ef4444' :
|
||||
priority === 'medium' ? '#f59e0b' : '#10b981'
|
||||
color:
|
||||
priority === 'high'
|
||||
? '#ef4444'
|
||||
: priority === 'medium'
|
||||
? '#f59e0b'
|
||||
: '#10b981',
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -329,30 +374,34 @@ export class MetricsService {
|
||||
/**
|
||||
* Récupère les métriques de vélocité d'équipe (pour graphiques de tendance)
|
||||
*/
|
||||
static async getVelocityTrends(weeksBack: number = 4): Promise<VelocityTrend[]> {
|
||||
static async getVelocityTrends(
|
||||
weeksBack: number = 4
|
||||
): Promise<VelocityTrend[]> {
|
||||
const trends = [];
|
||||
|
||||
|
||||
for (let i = weeksBack - 1; i >= 0; i--) {
|
||||
const weekStart = startOfWeek(subtractDays(getToday(), i * 7), { weekStartsOn: 1 });
|
||||
const weekStart = startOfWeek(subtractDays(getToday(), i * 7), {
|
||||
weekStartsOn: 1,
|
||||
});
|
||||
const weekEnd = endOfWeek(weekStart, { weekStartsOn: 1 });
|
||||
|
||||
|
||||
const [completed, created] = await Promise.all([
|
||||
prisma.task.count({
|
||||
where: {
|
||||
completedAt: {
|
||||
gte: weekStart,
|
||||
lte: weekEnd
|
||||
}
|
||||
}
|
||||
lte: weekEnd,
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.task.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: weekStart,
|
||||
lte: weekEnd
|
||||
}
|
||||
}
|
||||
})
|
||||
lte: weekEnd,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const velocity = created > 0 ? (completed / created) * 100 : 0;
|
||||
@@ -361,7 +410,7 @@ export class MetricsService {
|
||||
date: format(weekStart, 'dd/MM', { locale: fr }),
|
||||
completed,
|
||||
created,
|
||||
velocity: Math.round(velocity * 100) / 100
|
||||
velocity: Math.round(velocity * 100) / 100,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -47,13 +47,13 @@ export class TagAnalyticsService {
|
||||
* Calcule les métriques de distribution par tags
|
||||
*/
|
||||
static async getTagDistributionMetrics(
|
||||
timeRange?: TimeRange,
|
||||
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;
|
||||
|
||||
@@ -62,32 +62,30 @@ export class TagAnalyticsService {
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: start,
|
||||
lte: end
|
||||
}
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
}
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Récupérer aussi tous les tags pour avoir leurs couleurs
|
||||
const allTags = await prisma.tag.findMany({
|
||||
select: {
|
||||
name: true,
|
||||
color: true
|
||||
}
|
||||
color: true,
|
||||
},
|
||||
});
|
||||
|
||||
const tagColorMap = new Map(
|
||||
allTags.map(tag => [tag.name, tag.color])
|
||||
);
|
||||
|
||||
const tagColorMap = new Map(allTags.map((tag) => [tag.name, tag.color]));
|
||||
|
||||
// Convertir en format Task
|
||||
let tasks: Task[] = dbTasks.map(task => ({
|
||||
let tasks: Task[] = dbTasks.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
description: task.description || undefined,
|
||||
@@ -95,7 +93,7 @@ export class TagAnalyticsService {
|
||||
priority: task.priority as TaskPriority,
|
||||
source: task.source as TaskSource,
|
||||
sourceId: task.sourceId || undefined,
|
||||
tags: task.taskTags.map(taskTag => taskTag.tag.name),
|
||||
tags: task.taskTags.map((taskTag) => taskTag.tag.name),
|
||||
dueDate: task.dueDate || undefined,
|
||||
completedAt: task.completedAt || undefined,
|
||||
createdAt: task.createdAt,
|
||||
@@ -103,35 +101,40 @@ export class TagAnalyticsService {
|
||||
jiraProject: task.jiraProject || undefined,
|
||||
jiraKey: task.jiraKey || undefined,
|
||||
jiraType: task.jiraType || undefined,
|
||||
assignee: task.assignee || undefined
|
||||
assignee: task.assignee || undefined,
|
||||
}));
|
||||
|
||||
// Filtrer par sources si spécifié
|
||||
if (sources && sources.length > 0) {
|
||||
tasks = tasks.filter(task => sources.includes(task.source));
|
||||
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)
|
||||
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');
|
||||
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>) {
|
||||
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 => {
|
||||
tasks.forEach((task) => {
|
||||
task.tags.forEach((tagName) => {
|
||||
if (!tagCounts.has(tagName)) {
|
||||
const tagColor = getTagColor(tagName, tagColorMap.get(tagName));
|
||||
tagCounts.set(tagName, { color: tagColor, tasks: [] });
|
||||
@@ -141,19 +144,19 @@ export class TagAnalyticsService {
|
||||
});
|
||||
|
||||
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 => ({
|
||||
tasks: data.tasks.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
status: task.status,
|
||||
priority: task.priority
|
||||
}))
|
||||
priority: task.priority,
|
||||
})),
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
@@ -161,18 +164,24 @@ export class TagAnalyticsService {
|
||||
/**
|
||||
* 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;
|
||||
}>();
|
||||
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 => {
|
||||
tasks.forEach((task) => {
|
||||
task.tags.forEach((tagName) => {
|
||||
if (!tagMetrics.has(tagName)) {
|
||||
const tagColor = getTagColor(tagName, tagColorMap.get(tagName));
|
||||
tagMetrics.set(tagName, {
|
||||
@@ -180,20 +189,20 @@ export class TagAnalyticsService {
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
prioritySum: 0,
|
||||
priorityCount: 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;
|
||||
const priorityValue =
|
||||
task.priority === 'high' ? 3 : task.priority === 'medium' ? 2 : 1;
|
||||
metrics.prioritySum += priorityValue;
|
||||
metrics.priorityCount++;
|
||||
});
|
||||
@@ -204,10 +213,14 @@ export class TagAnalyticsService {
|
||||
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
|
||||
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
|
||||
@@ -218,11 +231,11 @@ export class TagAnalyticsService {
|
||||
*/
|
||||
private static calculateTagTrends(tasks: Task[]) {
|
||||
const trends = new Map<string, Map<string, number>>();
|
||||
|
||||
|
||||
// Grouper par jour et par tag
|
||||
tasks.forEach(task => {
|
||||
tasks.forEach((task) => {
|
||||
const day = task.createdAt.toISOString().split('T')[0];
|
||||
task.tags.forEach(tagName => {
|
||||
task.tags.forEach((tagName) => {
|
||||
if (!trends.has(tagName)) {
|
||||
trends.set(tagName, new Map());
|
||||
}
|
||||
@@ -248,27 +261,32 @@ export class TagAnalyticsService {
|
||||
private static calculateTagStats(tasks: Task[]) {
|
||||
const allTags = new Set<string>();
|
||||
const tagUsage = new Map<string, number>();
|
||||
|
||||
tasks.forEach(task => {
|
||||
task.tags.forEach(tagName => {
|
||||
|
||||
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';
|
||||
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,
|
||||
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
|
||||
avgTasksPerTag:
|
||||
allTags.size > 0
|
||||
? usageValues.reduce((sum, count) => sum + count, 0) / allTags.size
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user