- Added ownerId field to Task model to associate tasks with users. - Updated TaskService methods to enforce user ownership in task operations. - Enhanced API routes to include user authentication and ownership checks. - Modified DailyService and analytics services to filter tasks by user. - Integrated user session handling in various components for personalized task management.
237 lines
6.8 KiB
TypeScript
237 lines
6.8 KiB
TypeScript
import { TaskStatus } from '@/lib/types';
|
|
import { prisma } from '@/services/core/database';
|
|
import { getToday } from '@/lib/date-utils';
|
|
|
|
export interface DeadlineTask {
|
|
id: string;
|
|
title: string;
|
|
status: TaskStatus;
|
|
priority: string;
|
|
dueDate: Date;
|
|
daysRemaining: number;
|
|
urgencyLevel: 'overdue' | 'critical' | 'warning' | 'normal';
|
|
source: string;
|
|
tags: string[];
|
|
jiraKey?: string;
|
|
}
|
|
|
|
export interface DeadlineMetrics {
|
|
overdue: DeadlineTask[];
|
|
critical: DeadlineTask[]; // 0-2 jours
|
|
warning: DeadlineTask[]; // 3-7 jours
|
|
upcoming: DeadlineTask[]; // 8-14 jours
|
|
summary: {
|
|
overdueCount: number;
|
|
criticalCount: number;
|
|
warningCount: number;
|
|
upcomingCount: number;
|
|
totalWithDeadlines: number;
|
|
};
|
|
}
|
|
|
|
export class DeadlineAnalyticsService {
|
|
/**
|
|
* Analyse les tâches selon leurs échéances
|
|
*/
|
|
static async getDeadlineMetrics(
|
|
userId: string,
|
|
sources?: string[]
|
|
): Promise<DeadlineMetrics> {
|
|
try {
|
|
const now = getToday();
|
|
|
|
// Récupérer toutes les tâches non terminées avec échéance
|
|
const dbTasks = await prisma.task.findMany({
|
|
where: {
|
|
ownerId: userId,
|
|
dueDate: {
|
|
not: null,
|
|
},
|
|
status: {
|
|
notIn: ['done', 'cancelled', 'archived'],
|
|
},
|
|
},
|
|
include: {
|
|
taskTags: {
|
|
include: {
|
|
tag: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
dueDate: 'asc',
|
|
},
|
|
});
|
|
|
|
// Convertir et analyser les tâches
|
|
let deadlineTasks: DeadlineTask[] = dbTasks.map((task) => {
|
|
const dueDate = task.dueDate!;
|
|
const daysRemaining = Math.ceil(
|
|
(dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
|
);
|
|
|
|
let urgencyLevel: DeadlineTask['urgencyLevel'];
|
|
if (daysRemaining < 0) {
|
|
urgencyLevel = 'overdue';
|
|
} else if (daysRemaining <= 2) {
|
|
urgencyLevel = 'critical';
|
|
} else if (daysRemaining <= 7) {
|
|
urgencyLevel = 'warning';
|
|
} else {
|
|
urgencyLevel = 'normal';
|
|
}
|
|
|
|
return {
|
|
id: task.id,
|
|
title: task.title,
|
|
status: task.status as TaskStatus,
|
|
priority: task.priority,
|
|
dueDate,
|
|
daysRemaining,
|
|
urgencyLevel,
|
|
source: task.source,
|
|
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)
|
|
);
|
|
}
|
|
|
|
// Filtrer les tâches dans les 2 prochaines semaines
|
|
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
|
|
);
|
|
|
|
return {
|
|
overdue,
|
|
critical,
|
|
warning,
|
|
upcoming,
|
|
summary: {
|
|
overdueCount: overdue.length,
|
|
criticalCount: critical.length,
|
|
warningCount: warning.length,
|
|
upcomingCount: upcoming.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");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retourne les tâches les plus critiques (en retard + échéance dans 48h)
|
|
*/
|
|
static async getCriticalDeadlines(
|
|
userId: string,
|
|
sources?: string[]
|
|
): Promise<DeadlineTask[]> {
|
|
const metrics = await this.getDeadlineMetrics(userId, sources);
|
|
return [...metrics.overdue, ...metrics.critical].slice(0, 10); // Limite à 10 tâches les plus critiques
|
|
}
|
|
|
|
/**
|
|
* Analyse l'impact des échéances par priorité
|
|
*/
|
|
static analyzeImpactByPriority(tasks: DeadlineTask[]): Array<{
|
|
priority: string;
|
|
count: number;
|
|
overdueCount: number;
|
|
criticalCount: number;
|
|
}> {
|
|
const priorityGroups = new Map<string, DeadlineTask[]>();
|
|
|
|
tasks.forEach((task) => {
|
|
const priority = task.priority || 'medium';
|
|
if (!priorityGroups.has(priority)) {
|
|
priorityGroups.set(priority, []);
|
|
}
|
|
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)
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calcule les métriques de risque
|
|
*/
|
|
static calculateRiskMetrics(metrics: DeadlineMetrics): {
|
|
riskScore: number; // 0-100
|
|
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
|
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
|
|
|
|
// Limiter à 100
|
|
riskScore = Math.min(riskScore, 100);
|
|
|
|
let riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
|
let recommendation: string;
|
|
|
|
if (riskScore >= 75) {
|
|
riskLevel = 'critical';
|
|
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.';
|
|
} else if (riskScore >= 25) {
|
|
riskLevel = 'medium';
|
|
recommendation =
|
|
'Surveillance nécessaire, quelques échéances à surveiller.';
|
|
} else {
|
|
riskLevel = 'low';
|
|
recommendation = 'Situation stable, échéances sous contrôle.';
|
|
}
|
|
|
|
return { riskScore, riskLevel, recommendation };
|
|
}
|
|
}
|