feat: add 'Manager' link to Header component
- Introduced a new navigation link for the 'Manager' page in the Header component, improving user access to management features.
This commit is contained in:
563
services/manager-summary.ts
Normal file
563
services/manager-summary.ts
Normal file
@@ -0,0 +1,563 @@
|
||||
import { prisma } from './database';
|
||||
import { startOfWeek, endOfWeek } from 'date-fns';
|
||||
|
||||
type TaskType = {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
priority: string; // high, medium, low
|
||||
completedAt?: Date | null;
|
||||
createdAt: Date;
|
||||
taskTags?: {
|
||||
tag: {
|
||||
name: string;
|
||||
}
|
||||
}[];
|
||||
};
|
||||
|
||||
type CheckboxType = {
|
||||
id: string;
|
||||
text: string;
|
||||
isChecked: boolean;
|
||||
type: string; // task, meeting
|
||||
date: Date;
|
||||
createdAt: Date;
|
||||
task?: {
|
||||
id: string;
|
||||
title: string;
|
||||
priority: string;
|
||||
taskTags?: {
|
||||
tag: {
|
||||
name: string;
|
||||
}
|
||||
}[];
|
||||
} | null;
|
||||
};
|
||||
|
||||
export interface KeyAccomplishment {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
impact: 'high' | 'medium' | 'low';
|
||||
completedAt: Date;
|
||||
relatedItems: string[]; // IDs des tâches/checkboxes liées
|
||||
todosCount: number; // Nombre de todos associés
|
||||
}
|
||||
|
||||
export interface UpcomingChallenge {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
estimatedEffort: 'days' | 'weeks' | 'hours';
|
||||
blockers: string[];
|
||||
deadline?: Date;
|
||||
relatedItems: string[]; // IDs des tâches/checkboxes liées
|
||||
todosCount: number; // Nombre de todos associés
|
||||
}
|
||||
|
||||
export interface ManagerSummary {
|
||||
period: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
keyAccomplishments: KeyAccomplishment[];
|
||||
upcomingChallenges: UpcomingChallenge[];
|
||||
metrics: {
|
||||
totalTasksCompleted: number;
|
||||
totalCheckboxesCompleted: number;
|
||||
highPriorityTasksCompleted: number;
|
||||
meetingCheckboxesCompleted: number;
|
||||
completionRate: number;
|
||||
focusAreas: { [category: string]: number };
|
||||
};
|
||||
narrative: {
|
||||
weekHighlight: string;
|
||||
mainChallenges: string;
|
||||
nextWeekFocus: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ManagerSummaryService {
|
||||
/**
|
||||
* Génère un résumé orienté manager pour la semaine
|
||||
*/
|
||||
static async getManagerSummary(date: Date = new Date()): Promise<ManagerSummary> {
|
||||
const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi
|
||||
const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche
|
||||
|
||||
// Récupérer les données de base
|
||||
const [tasks, checkboxes] = await Promise.all([
|
||||
this.getCompletedTasks(weekStart, weekEnd),
|
||||
this.getCompletedCheckboxes(weekStart, weekEnd)
|
||||
]);
|
||||
|
||||
// Analyser et extraire les accomplissements clés
|
||||
const keyAccomplishments = 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);
|
||||
|
||||
return {
|
||||
period: { start: weekStart, end: weekEnd },
|
||||
keyAccomplishments,
|
||||
upcomingChallenges,
|
||||
metrics,
|
||||
narrative
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les tâches complétées de la semaine
|
||||
*/
|
||||
private static async getCompletedTasks(startDate: Date, endDate: Date) {
|
||||
const tasks = await prisma.task.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
// Tâches avec completedAt dans la période (priorité)
|
||||
{
|
||||
completedAt: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
}
|
||||
},
|
||||
// Tâches avec status 'done' et updatedAt dans la période
|
||||
{
|
||||
status: 'done',
|
||||
updatedAt: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
}
|
||||
},
|
||||
// Tâches avec status 'archived' récemment (aussi des accomplissements)
|
||||
{
|
||||
status: 'archived',
|
||||
updatedAt: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: {
|
||||
completedAt: 'desc'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
priority: true,
|
||||
completedAt: true,
|
||||
createdAt: true,
|
||||
taskTags: {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les checkboxes complétées de la semaine
|
||||
*/
|
||||
private static async getCompletedCheckboxes(startDate: Date, endDate: Date) {
|
||||
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||
where: {
|
||||
isChecked: true,
|
||||
date: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
isChecked: true,
|
||||
type: true,
|
||||
date: true,
|
||||
createdAt: true,
|
||||
task: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
priority: true,
|
||||
taskTags: {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return checkboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait les accomplissements clés basés sur la priorité
|
||||
*/
|
||||
private static extractKeyAccomplishments(tasks: TaskType[], checkboxes: CheckboxType[]): KeyAccomplishment[] {
|
||||
const accomplishments: KeyAccomplishment[] = [];
|
||||
|
||||
// Tâches: prendre toutes les high/medium priority, et quelques low si significatives
|
||||
tasks.forEach(task => {
|
||||
const priority = task.priority.toLowerCase();
|
||||
|
||||
// Convertir priorité task en impact accomplissement
|
||||
let impact: 'high' | 'medium' | 'low';
|
||||
if (priority === 'high') {
|
||||
impact = 'high';
|
||||
} else if (priority === 'medium') {
|
||||
impact = 'medium';
|
||||
} else {
|
||||
// Pour les low priority, ne garder que si c'est vraiment significatif
|
||||
if (!this.isSignificantTask(task.title)) {
|
||||
return;
|
||||
}
|
||||
impact = 'low';
|
||||
}
|
||||
|
||||
// Compter les todos (checkboxes) associés à cette tâche
|
||||
const relatedTodos = checkboxes.filter(cb => cb.task?.id === task.id);
|
||||
|
||||
accomplishments.push({
|
||||
id: `task-${task.id}`,
|
||||
title: task.title,
|
||||
description: task.description || undefined,
|
||||
tags: task.taskTags?.map(tt => tt.tag.name) || [],
|
||||
impact,
|
||||
completedAt: task.completedAt || new Date(),
|
||||
relatedItems: [task.id, ...relatedTodos.map(t => t.id)],
|
||||
todosCount: relatedTodos.length // Nombre réel de todos associés
|
||||
});
|
||||
});
|
||||
|
||||
// AJOUTER SEULEMENT les meetings importants standalone (non liés à une tâche)
|
||||
const standaloneMeetings = checkboxes.filter(checkbox =>
|
||||
checkbox.type === 'meeting' && !checkbox.task // Meetings non liés à une tâche
|
||||
);
|
||||
|
||||
standaloneMeetings.forEach(meeting => {
|
||||
accomplishments.push({
|
||||
id: `meeting-${meeting.id}`,
|
||||
title: `📅 ${meeting.text}`,
|
||||
tags: [], // Meetings n'ont pas de tags par défaut
|
||||
impact: 'medium', // Meetings sont importants
|
||||
completedAt: meeting.date,
|
||||
relatedItems: [meeting.id],
|
||||
todosCount: 1 // Un meeting = 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();
|
||||
})
|
||||
.slice(0, 12); // Plus d'items maintenant qu'on filtre mieux
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifie les défis et enjeux à venir
|
||||
*/
|
||||
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
|
||||
},
|
||||
orderBy: [
|
||||
{ priority: 'asc' }, // high < medium < low
|
||||
{ createdAt: 'desc' }
|
||||
],
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
priority: true,
|
||||
createdAt: true,
|
||||
taskTags: {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
take: 30
|
||||
});
|
||||
|
||||
// Récupérer les checkboxes récurrentes non complétées (meetings + tâches prioritaires)
|
||||
const upcomingCheckboxes = await prisma.dailyCheckbox.findMany({
|
||||
where: {
|
||||
isChecked: false,
|
||||
date: {
|
||||
gte: new Date()
|
||||
},
|
||||
OR: [
|
||||
{ type: 'meeting' },
|
||||
{
|
||||
task: {
|
||||
priority: {
|
||||
in: ['high', 'medium']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
isChecked: true,
|
||||
type: true,
|
||||
date: true,
|
||||
createdAt: true,
|
||||
task: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
priority: true,
|
||||
taskTags: {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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') {
|
||||
priority = 'high';
|
||||
} else if (taskPriority === 'medium') {
|
||||
priority = 'medium';
|
||||
} else {
|
||||
// Pour les low priority, ne garder que si c'est vraiment challengeant
|
||||
if (!this.isChallengingTask(task.title)) {
|
||||
return;
|
||||
}
|
||||
priority = 'low';
|
||||
}
|
||||
|
||||
const estimatedEffort = this.estimateEffort(task.title, task.description || undefined);
|
||||
|
||||
challenges.push({
|
||||
id: `task-${task.id}`,
|
||||
title: task.title,
|
||||
description: task.description || undefined,
|
||||
tags: task.taskTags?.map(tt => tt.tag.name) || [],
|
||||
priority,
|
||||
estimatedEffort,
|
||||
blockers: this.identifyBlockers(task.title, task.description || undefined),
|
||||
relatedItems: [task.id],
|
||||
todosCount: 0 // TODO: compter les todos associés à cette tâche
|
||||
});
|
||||
});
|
||||
|
||||
// Ajouter les meetings importants comme challenges
|
||||
upcomingCheckboxes.forEach(checkbox => {
|
||||
if (checkbox.type === 'meeting') {
|
||||
challenges.push({
|
||||
id: `checkbox-${checkbox.id}`,
|
||||
title: checkbox.text,
|
||||
tags: checkbox.task?.taskTags?.map(tt => tt.tag.name) || [],
|
||||
priority: 'medium', // Meetings sont medium par défaut
|
||||
estimatedEffort: 'hours',
|
||||
blockers: [],
|
||||
relatedItems: [checkbox.id],
|
||||
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];
|
||||
})
|
||||
.slice(0, 10); // Plus d'items maintenant qu'on filtre mieux
|
||||
}
|
||||
|
||||
/**
|
||||
* Estime l'effort requis
|
||||
*/
|
||||
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')) {
|
||||
return 'weeks';
|
||||
}
|
||||
if (content.includes('feature') || content.includes('implement') || content.includes('integration')) {
|
||||
return 'days';
|
||||
}
|
||||
return 'hours';
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifie les blockers potentiels
|
||||
*/
|
||||
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');
|
||||
}
|
||||
if (content.includes('approval') || content.includes('review')) {
|
||||
blockers.push('Validation requise');
|
||||
}
|
||||
if (content.includes('design') && !content.includes('implement')) {
|
||||
blockers.push('Spécifications incomplètes');
|
||||
}
|
||||
|
||||
return blockers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine si une tâche est significative
|
||||
*/
|
||||
private static isSignificantTask(title: string): boolean {
|
||||
const significantKeywords = [
|
||||
'release', 'deploy', 'launch', 'milestone',
|
||||
'architecture', 'design', 'strategy',
|
||||
'integration', 'migration', 'optimization'
|
||||
];
|
||||
return significantKeywords.some(keyword => title.toLowerCase().includes(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine si une checkbox est significative
|
||||
*/
|
||||
private static isSignificantCheckbox(text: string): boolean {
|
||||
const content = text.toLowerCase();
|
||||
return content.length > 30 || // Checkboxes détaillées
|
||||
content.includes('meeting') ||
|
||||
content.includes('review') ||
|
||||
content.includes('call') ||
|
||||
content.includes('presentation');
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine si une tâche représente un défi
|
||||
*/
|
||||
private static isChallengingTask(title: string): boolean {
|
||||
const challengingKeywords = [
|
||||
'complex', 'difficult', 'challenge',
|
||||
'architecture', 'performance', 'security',
|
||||
'integration', 'migration', 'optimization'
|
||||
];
|
||||
return challengingKeywords.some(keyword => title.toLowerCase().includes(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse les patterns dans les checkboxes pour identifier des enjeux
|
||||
*/
|
||||
private static analyzeCheckboxPatterns(): UpcomingChallenge[] {
|
||||
// Pour l'instant, retourner un array vide
|
||||
// À implémenter selon les besoins spécifiques
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les métriques résumées
|
||||
*/
|
||||
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;
|
||||
|
||||
// Analyser la répartition par catégorie
|
||||
const focusAreas: { [category: string]: number } = {};
|
||||
|
||||
return {
|
||||
totalTasksCompleted,
|
||||
totalCheckboxesCompleted,
|
||||
highPriorityTasksCompleted,
|
||||
meetingCheckboxesCompleted,
|
||||
completionRate: 0, // À calculer par rapport aux objectifs
|
||||
focusAreas
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère le narratif pour le manager
|
||||
*/
|
||||
private static generateNarrative(
|
||||
accomplishments: KeyAccomplishment[],
|
||||
challenges: UpcomingChallenge[]
|
||||
) {
|
||||
// Points forts de la semaine
|
||||
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.';
|
||||
|
||||
// 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.';
|
||||
|
||||
// Focus semaine prochaine
|
||||
const topChallenges = challenges.slice(0, 3);
|
||||
const nextWeekFocus = topChallenges.length > 0
|
||||
? `La semaine prochaine sera concentrée sur ${topChallenges.map(c => c.title).join(', ')}.`
|
||||
: 'Continuation du travail en cours selon les priorités établies.';
|
||||
|
||||
return {
|
||||
weekHighlight,
|
||||
mainChallenges,
|
||||
nextWeekFocus
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user