chore: prettier everywhere

This commit is contained in:
Julien Froidefond
2025-10-09 13:40:03 +02:00
parent f8100ae3e9
commit d9cf9a2655
303 changed files with 15420 additions and 9391 deletions

View File

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

View File

@@ -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.';

View File

@@ -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 focalie 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,
};
}
}

View File

@@ -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,
});
}

View File

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