feat: add weekly summary features and components
- Introduced `CategoryBreakdown`, `JiraWeeklyMetrics`, `PeriodSelector`, and `VelocityMetrics` components to enhance the weekly summary dashboard. - Updated `WeeklySummaryClient` to manage period selection and PDF export functionality. - Enhanced `WeeklySummaryService` to support period comparisons and activity categorization. - Added new API route for fetching weekly summary data based on selected period. - Updated `package.json` and `package-lock.json` to include `jspdf` and related types for PDF generation. - Marked several tasks as complete in `TODO.md` to reflect progress on summary features.
This commit is contained in:
185
services/task-categorization.ts
Normal file
185
services/task-categorization.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
export interface PredefinedCategory {
|
||||
name: string;
|
||||
color: string;
|
||||
keywords: string[];
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const PREDEFINED_CATEGORIES: PredefinedCategory[] = [
|
||||
{
|
||||
name: 'Dev',
|
||||
color: '#3b82f6', // Blue
|
||||
icon: '💻',
|
||||
keywords: [
|
||||
'code', 'coding', 'development', 'develop', 'dev', 'programming', 'program',
|
||||
'bug', 'fix', 'debug', 'feature', 'implement', 'refactor', 'review',
|
||||
'api', 'database', 'db', 'frontend', 'backend', 'ui', 'ux',
|
||||
'component', 'service', 'function', 'method', 'class',
|
||||
'git', 'commit', 'merge', 'pull request', 'pr', 'deploy', 'deployment',
|
||||
'test', 'testing', 'unit test', 'integration'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Meeting',
|
||||
color: '#8b5cf6', // Purple
|
||||
icon: '🤝',
|
||||
keywords: [
|
||||
'meeting', 'réunion', 'call', 'standup', 'daily', 'retrospective', 'retro',
|
||||
'planning', 'demo', 'presentation', 'sync', 'catch up', 'catchup',
|
||||
'interview', 'discussion', 'brainstorm', 'workshop', 'session',
|
||||
'one on one', '1on1', 'review meeting', 'sprint planning'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Admin',
|
||||
color: '#6b7280', // Gray
|
||||
icon: '📋',
|
||||
keywords: [
|
||||
'admin', 'administration', 'paperwork', 'documentation', 'doc', 'docs',
|
||||
'report', 'reporting', 'timesheet', 'expense', 'invoice',
|
||||
'email', 'mail', 'communication', 'update', 'status',
|
||||
'config', 'configuration', 'setup', 'installation', 'maintenance',
|
||||
'backup', 'security', 'permission', 'user management'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Learning',
|
||||
color: '#10b981', // Green
|
||||
icon: '📚',
|
||||
keywords: [
|
||||
'learning', 'learn', 'study', 'training', 'course', 'tutorial',
|
||||
'research', 'reading', 'documentation', 'knowledge', 'skill',
|
||||
'certification', 'workshop', 'seminar', 'conference',
|
||||
'practice', 'exercise', 'experiment', 'exploration', 'investigate'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export class TaskCategorizationService {
|
||||
/**
|
||||
* Suggère une catégorie basée sur le titre et la description d'une tâche
|
||||
*/
|
||||
static suggestCategory(title: string, description?: string): PredefinedCategory | null {
|
||||
const text = `${title} ${description || ''}`.toLowerCase();
|
||||
|
||||
// Compte les matches pour chaque catégorie
|
||||
const categoryScores = PREDEFINED_CATEGORIES.map(category => {
|
||||
const matches = category.keywords.filter(keyword =>
|
||||
text.includes(keyword.toLowerCase())
|
||||
).length;
|
||||
|
||||
return {
|
||||
category,
|
||||
score: matches
|
||||
};
|
||||
});
|
||||
|
||||
// Trouve la meilleure catégorie
|
||||
const bestMatch = categoryScores.reduce((best, current) =>
|
||||
current.score > best.score ? current : best
|
||||
);
|
||||
|
||||
// Retourne la catégorie seulement s'il y a au moins un match
|
||||
return bestMatch.score > 0 ? bestMatch.category : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggère plusieurs catégories avec leur score de confiance
|
||||
*/
|
||||
static suggestCategoriesWithScore(title: string, description?: string): Array<{
|
||||
category: PredefinedCategory;
|
||||
score: number;
|
||||
confidence: number;
|
||||
}> {
|
||||
const text = `${title} ${description || ''}`.toLowerCase();
|
||||
|
||||
const categoryScores = PREDEFINED_CATEGORIES.map(category => {
|
||||
const matches = category.keywords.filter(keyword =>
|
||||
text.includes(keyword.toLowerCase())
|
||||
);
|
||||
|
||||
const score = matches.length;
|
||||
const confidence = Math.min((score / 3) * 100, 100); // Max 100% de confiance avec 3+ mots-clés
|
||||
|
||||
return {
|
||||
category,
|
||||
score,
|
||||
confidence
|
||||
};
|
||||
});
|
||||
|
||||
return categoryScores
|
||||
.filter(item => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse les activités et retourne la répartition par catégorie
|
||||
*/
|
||||
static analyzeActivitiesByCategory(activities: Array<{ title: string; description?: string }>): {
|
||||
[categoryName: string]: {
|
||||
count: number;
|
||||
percentage: number;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
} {
|
||||
const categoryCounts: { [key: string]: number } = {};
|
||||
const uncategorized = { count: 0 };
|
||||
|
||||
// Initialiser les compteurs
|
||||
PREDEFINED_CATEGORIES.forEach(cat => {
|
||||
categoryCounts[cat.name] = 0;
|
||||
});
|
||||
|
||||
// Analyser chaque activité
|
||||
activities.forEach(activity => {
|
||||
const suggestedCategory = this.suggestCategory(activity.title, activity.description);
|
||||
|
||||
if (suggestedCategory) {
|
||||
categoryCounts[suggestedCategory.name]++;
|
||||
} else {
|
||||
uncategorized.count++;
|
||||
}
|
||||
});
|
||||
|
||||
const total = activities.length;
|
||||
const result: { [categoryName: string]: { count: number; percentage: number; color: string; icon: string } } = {};
|
||||
|
||||
// Ajouter les catégories prédéfinies
|
||||
PREDEFINED_CATEGORIES.forEach(category => {
|
||||
const count = categoryCounts[category.name];
|
||||
result[category.name] = {
|
||||
count,
|
||||
percentage: total > 0 ? (count / total) * 100 : 0,
|
||||
color: category.color,
|
||||
icon: category.icon
|
||||
};
|
||||
});
|
||||
|
||||
// Ajouter "Autre" si nécessaire
|
||||
if (uncategorized.count > 0) {
|
||||
result['Autre'] = {
|
||||
count: uncategorized.count,
|
||||
percentage: total > 0 ? (uncategorized.count / total) * 100 : 0,
|
||||
color: '#d1d5db',
|
||||
icon: '❓'
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les tags suggérés pour une tâche
|
||||
*/
|
||||
static getSuggestedTags(title: string, description?: string): string[] {
|
||||
const suggestions = this.suggestCategoriesWithScore(title, description);
|
||||
|
||||
return suggestions
|
||||
.filter(s => s.confidence >= 30) // Seulement les suggestions avec 30%+ de confiance
|
||||
.slice(0, 2) // Maximum 2 suggestions
|
||||
.map(s => s.category.name);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user