+ {summaryItems.map((item, index) => (
+
+
+
+ {item.icon}
+
+
{item.label}
+
+
+ {item.count}
+
+
+ ))}
+
+ {/* Indicateur de performance */}
+
+
+ Tâches sous contrôle:
+
+ {summary.totalWithDeadlines - summary.overdueCount - summary.criticalCount}/{summary.totalWithDeadlines}
+
+
+
+
0
+ ? Math.round(((summary.totalWithDeadlines - summary.overdueCount - summary.criticalCount) / summary.totalWithDeadlines) * 100)
+ : 100
+ }%`
+ }}
+ />
+
+
+
+
+ );
+}
diff --git a/src/components/deadline/index.ts b/src/components/deadline/index.ts
new file mode 100644
index 0000000..4f93831
--- /dev/null
+++ b/src/components/deadline/index.ts
@@ -0,0 +1,4 @@
+export { DeadlineOverview } from './DeadlineOverview';
+export { DeadlineRiskCard } from './DeadlineRiskCard';
+export { CriticalDeadlinesCard } from './CriticalDeadlinesCard';
+export { DeadlineSummaryCard } from './DeadlineSummaryCard';
diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts
index a2e373b..ff758c4 100644
--- a/src/lib/date-utils.ts
+++ b/src/lib/date-utils.ts
@@ -3,7 +3,7 @@
* Regroupe toutes les fonctions de formatage, manipulation et validation de dates
*/
-import { format, startOfDay, endOfDay, isValid } from 'date-fns';
+import { format, startOfDay, endOfDay, isValid, formatDistanceToNow as formatDistanceToNowFns } from 'date-fns';
import { fr } from 'date-fns/locale';
// Re-export des utilitaires workday existants
@@ -244,3 +244,17 @@ export function generateDateTitle(date: Date, emoji: string = '📅'): string {
return `${emoji} ${formatDateShort(date)}`;
}
+
+/**
+ * Formate la distance depuis maintenant en français
+ */
+export function formatDistanceToNow(date: Date, options?: { addSuffix?: boolean }): string {
+ if (!isValid(date)) {
+ throw new Error('Date invalide fournie à formatDistanceToNow');
+ }
+
+ return formatDistanceToNowFns(date, {
+ locale: fr,
+ addSuffix: options?.addSuffix ?? true
+ });
+}
diff --git a/src/services/analytics/analytics.ts b/src/services/analytics/analytics.ts
index 1e8391a..64090ed 100644
--- a/src/services/analytics/analytics.ts
+++ b/src/services/analytics/analytics.ts
@@ -13,6 +13,8 @@ export interface ProductivityMetrics {
week: string;
completed: number;
average: number;
+ weekNumber?: number;
+ weekStart?: Date;
}>;
priorityDistribution: Array<{
priority: string;
@@ -137,35 +139,41 @@ 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 }> = [];
+ const weeklyData: Array<{ week: string; completed: number; average: number; weekNumber: number; weekStart: Date }> = [];
const completedTasks = tasks.filter(task => task.completedAt);
- // Grouper par semaine
- const weekGroups = new Map
();
+ // Grouper par semaine en utilisant le numéro de semaine comme clé
+ const weekGroups = new Map();
completedTasks.forEach(task => {
if (task.completedAt && task.completedAt >= start && task.completedAt <= end) {
const weekStart = this.getWeekStart(task.completedAt);
- const weekKey = weekStart.toISOString().split('T')[0];
- weekGroups.set(weekKey, (weekGroups.get(weekKey) || 0) + 1);
+ 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());
+ 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((count, weekKey) => {
- const weekDate = parseDate(weekKey);
+ weekGroups.forEach((data, weekNumber) => {
weeklyData.push({
- week: `Sem. ${this.getWeekNumber(weekDate)}`,
- completed: count,
- average: Math.round(average * 10) / 10
+ week: `Sem. ${weekNumber}`,
+ completed: data.count,
+ average: Math.round(average * 10) / 10,
+ weekNumber,
+ weekStart: data.weekStart
});
});
- return weeklyData.sort((a, b) => a.week.localeCompare(b.week));
+ // Trier par numéro de semaine (pas alphabétiquement)
+ return weeklyData.sort((a, b) => a.weekNumber - b.weekNumber);
}
/**
diff --git a/src/services/analytics/deadline-analytics.ts b/src/services/analytics/deadline-analytics.ts
new file mode 100644
index 0000000..264304e
--- /dev/null
+++ b/src/services/analytics/deadline-analytics.ts
@@ -0,0 +1,206 @@
+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(): Promise {
+ try {
+ const now = getToday();
+
+ // Récupérer toutes les tâches non terminées avec échéance
+ const dbTasks = await prisma.task.findMany({
+ where: {
+ dueDate: {
+ not: null
+ },
+ status: {
+ notIn: ['done', 'cancelled', 'archived']
+ }
+ },
+ include: {
+ taskTags: {
+ include: {
+ tag: true
+ }
+ }
+ },
+ orderBy: {
+ dueDate: 'asc'
+ }
+ });
+
+ // Convertir et analyser les tâches
+ const 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 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(): Promise {
+ const metrics = await this.getDeadlineMetrics();
+ 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();
+
+ 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 = { 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 };
+ }
+}
diff --git a/src/services/analytics/manager-summary.ts b/src/services/analytics/manager-summary.ts
index 91873c8..5f7acf8 100644
--- a/src/services/analytics/manager-summary.ts
+++ b/src/services/analytics/manager-summary.ts
@@ -1,5 +1,4 @@
import { prisma } from '@/services/core/database';
-import { startOfWeek, endOfWeek } from 'date-fns';
import { getToday } from '@/lib/date-utils';
type TaskType = {
@@ -83,11 +82,13 @@ export interface ManagerSummary {
export class ManagerSummaryService {
/**
- * Génère un résumé orienté manager pour la semaine
+ * Génère un résumé orienté manager pour les 7 derniers jours
*/
static async getManagerSummary(date: Date = getToday()): Promise {
- const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi
- const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche
+ // 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([
@@ -537,22 +538,22 @@ export class ManagerSummaryService {
accomplishments: KeyAccomplishment[],
challenges: UpcomingChallenge[]
) {
- // Points forts de la semaine
+ // Points forts des 7 derniers jours
const topAccomplishments = accomplishments.slice(0, 3);
const weekHighlight = topAccomplishments.length > 0
- ? `Cette semaine, j'ai principalement progressé sur ${topAccomplishments.map(a => a.title).join(', ')}.`
- : 'Semaine focalisée sur l\'exécution des tâches quotidiennes.';
+ ? `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 cette semaine.';
+ : 'Pas de blockers majeurs rencontrés sur cette période.';
- // Focus semaine prochaine
+ // Focus 7 prochains jours
const topChallenges = challenges.slice(0, 3);
const nextWeekFocus = topChallenges.length > 0
- ? `La semaine prochaine sera concentrée sur ${topChallenges.map(c => c.title).join(', ')}.`
+ ? `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 {
diff --git a/src/services/analytics/metrics.ts b/src/services/analytics/metrics.ts
index 549c122..e3cc0d8 100644
--- a/src/services/analytics/metrics.ts
+++ b/src/services/analytics/metrics.ts
@@ -1,5 +1,5 @@
import { prisma } from '@/services/core/database';
-import { startOfWeek, endOfWeek, eachDayOfInterval, format, startOfDay, endOfDay } 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';
@@ -57,11 +57,13 @@ export interface WeeklyMetricsOverview {
export class MetricsService {
/**
- * Récupère les métriques journalières de la semaine
+ * Récupère les métriques journalières des 7 derniers jours
*/
static async getWeeklyMetrics(date: Date = getToday()): Promise {
- const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi
- const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche
+ // 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 });