diff --git a/TODO.md b/TODO.md index cf125c5..1175909 100644 --- a/TODO.md +++ b/TODO.md @@ -305,134 +305,6 @@ Endpoints complexes → API Routes conservées - [x] Vue détaillée par sprint avec drill-down - [x] ~~Intégration avec les daily notes (mentions des blockers)~~ (supprimé) -## 📊 Phase 5.6: Résumé hebdomadaire pour Individual Review (EN COURS) - -### 5.6.1 Fonctionnalités de base (TERMINÉ) -- [x] Vue résumé des 7 derniers jours (daily items + tâches) -- [x] Statistiques globales (completion rates, jour le plus productif) -- [x] Timeline chronologique des activités -- [x] Filtrage par jour de la semaine -- [x] Architecture SSR pour performance optimale - -### 5.6.2 Améliorations pour l'Individual Review Manager 🎯 -- [ ] **Métriques de performance personnelles** - - [ ] Vélocité hebdomadaire (tasks completed/week) - - [ ] Temps moyen de completion des tâches - - [ ] Répartition par priorité (high/medium/low tasks) - - [ ] Taux de respect des deadlines - - [ ] Evolution des performances sur 4 semaines (tendance) - -- [ ] **Catégorisation des activités professionnelles** - - [ ] Auto-tagging par type : "Development", "Meetings", "Documentation", "Code Review" - - [ ] Répartition temps par catégorie (% dev vs meetings vs admin) - - [ ] Identification des "deep work" sessions vs interruptions - - [ ] Tracking des objectifs OKRs/KPIs assignés - -- [ ] **Visualisations pour manager** - - [ ] Graphique en aires : progression hebdomadaire - - [ ] Heatmap de productivité : heures/jours les plus productifs - - [ ] Radar chart : compétences/domaines travaillés - - [ ] Burndown chart personnel : objectifs vs réalisé - -- [ ] **Rapport automatique formaté** - - [ ] Export PDF professionnel avec métriques - - [ ] Template "Weekly Accomplishments" pré-rempli - - [ ] Bullet points des principales réalisations - - [ ] Section "Challenges & Blockers" automatique - - [ ] Recommandations d'amélioration basées sur les patterns - -- [ ] **Contexte business et impact** - - [ ] Liaison tâches → tickets Jira → business value - - [ ] Calcul d'impact estimé (story points, business priority) - - [ ] Suivi des initiatives stratégiques - - [ ] Corrélation avec les métriques d'équipe - -- [ ] **Intelligence et insights** - - [ ] Détection patterns de productivité personnels - - [ ] Suggestions d'optimisation du planning - - [ ] Alertes sur la charge de travail excessive - - [ ] Comparaison avec moyennes d'équipe (anonyme) - - [ ] Prédiction de capacity pour la semaine suivante - -- [ ] **Fonctionnalités avancées pour 1-on-1** - - [ ] Mode "Manager View" : vue consolidée pour discussions - - [ ] Annotations et notes privées sur les réalisations - - [ ] Objectifs SMART tracking avec progress bars - - [ ] Archivage des reviews précédentes pour suivi long terme - - [ ] Templates de questions pour auto-reflection - -### 5.6.3 Intégrations externes pour contexte pro -- [ ] **Import calendrier** : Meetings duration & frequency -- [ ] **GitHub/GitLab integration** : Commits, PRs, code reviews -- [ ] **Slack integration** : Messages envoyés, réactions, temps de réponse -- [ ] **Confluence/Notion** : Documents créés/édités -- [ ] **Time tracking tools** : Import depuis Toggl, Clockify, etc. - -### 5.6.4 Machine Learning & Predictions -- [ ] **Modèle de productivité personnelle** - - [ ] Prédiction des jours de forte/faible productivité - - [ ] Recommandations de planning optimal - - [ ] Détection automatique de burnout patterns - - [ ] Suggestions de breaks et équilibre work-life - -- [ ] **Insights business automatiques** - - [ ] "Cette semaine, tu as contribué à 3 initiatives stratégiques" - - [ ] "Ton focus sur la qualité (code reviews) est 20% au-dessus de la moyenne" - - [ ] "Suggestion: bloquer 2h demain pour deep work sur Project X" - -### 🚀 Quick Wins pour démarrer (Priorité 1) ✅ TERMINÉ -- [x] **Métriques de vélocité personnelle** (1-2h) - - [x] Calcul tâches complétées par jour/semaine - - [x] Graphique simple ligne de tendance sur 4 semaines - - [x] Comparaison semaine actuelle vs semaine précédente - -- [x] **Export PDF basique** (2-3h) - - [x] Génération PDF simple avec statistiques actuelles - - [x] Template "Weekly Summary" avec logo/header pro - - [x] Liste des principales réalisations de la semaine - -- [x] **Catégorisation simple par tags** (1h) - - [x] Tags prédéfinis : "Dev", "Meeting", "Admin", "Learning" - - [x] Auto-suggestion basée sur mots-clés dans les titres - - [x] Répartition en camembert par catégorie - -- [x] **Connexion Jira pour contexte business** ~~(supprimé par demande utilisateur)~~ - - ~~[x] Affichage des story points complétés~~ - - ~~[x] Lien vers les tickets Jira depuis les tâches~~ - - ~~[x] Récap des sprints/epics contributés~~ - -- [x] **Période flexible** (1h) - - [x] Sélecteur de période : dernière semaine, 2 semaines, mois - - [x] Comparaison période courante vs période précédente - - [x] Sauvegarde de la période préférée - -### 💡 Idées spécifiques pour Individual Review - -#### **Sections du rapport idéal :** -1. **Executive Summary** (3-4 bullet points impact business) -2. **Quantified Achievements** (metrics, numbers, scope) -3. **Technical Contributions** (code, architecture, tools) -4. **Collaboration Impact** (reviews, mentoring, knowledge sharing) -5. **Process Improvements** (efficiency gains, automation) -6. **Learning & Growth** (new skills, certifications, initiatives) -7. **Challenges & Solutions** (blockers overcome, lessons learned) -8. **Next Period Goals** (SMART objectives, capacity planning) - -#### **Métriques qui impressionnent un manager :** -- **Velocity & Consistency** : "Completed 23 tasks with 94% on-time delivery" -- **Quality Focus** : "15 code reviews provided, 0 production bugs" -- **Initiative** : "Automated deployment reducing release time by 30%" -- **Business Impact** : "Features delivered serve 10K+ users daily" -- **Collaboration** : "Mentored 2 junior devs, led 3 technical sessions" -- **Efficiency** : "Process optimization saved team 5h/week" - -#### **Questions auto-reflection intégrées :** -- "What was your biggest technical achievement this week?" -- "Which tasks had the highest business impact?" -- "What blockers did you encounter and how did you solve them?" -- "What did you learn that you can share with the team?" -- "What would you do differently next week?" - ## Autre Todos #2 - [ ] Synchro Jira auto en background timé comme pour la synchro de sauvegarde - [ ] refacto des allpreferences : ca devrait eter un contexte dans le layout qui balance serverside dans le hook diff --git a/components/dashboard/PeriodSelector.tsx b/components/dashboard/PeriodSelector.tsx deleted file mode 100644 index 6c5ec2e..0000000 --- a/components/dashboard/PeriodSelector.tsx +++ /dev/null @@ -1,155 +0,0 @@ -'use client'; - -import { PERIOD_OPTIONS, PeriodOption, PeriodComparison } from '@/services/weekly-summary'; -import { Card, CardHeader, CardContent } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; - -interface PeriodSelectorProps { - currentPeriod: PeriodOption; - onPeriodChange: (period: PeriodOption) => void; - comparison: PeriodComparison | null; - isLoading?: boolean; -} - -export function PeriodSelector({ - currentPeriod, - onPeriodChange, - comparison, - isLoading = false -}: PeriodSelectorProps) { - - const formatChange = (change: number): string => { - const sign = change > 0 ? '+' : ''; - return `${sign}${change.toFixed(1)}%`; - }; - - const getChangeColor = (change: number): string => { - if (change > 10) return 'text-[var(--success)]'; - if (change < -10) return 'text-[var(--destructive)]'; - return 'text-[var(--muted-foreground)]'; - }; - - const getChangeIcon = (change: number): string => { - if (change > 10) return '📈'; - if (change < -10) return '📉'; - return '➡️'; - }; - - return ( - - -
-

📊 Sélection de période

-
- {PERIOD_OPTIONS.map((option) => ( - - ))} -
-
-
- - {comparison && ( - -
- {/* Titre de comparaison */} -

- Comparaison avec la période précédente -

- - {/* Métriques de comparaison */} -
- {/* Tâches */} -
-
- Tâches complétées - - {getChangeIcon(comparison.changes.tasks)} {formatChange(comparison.changes.tasks)} - -
-
- - Actuelle: {comparison.currentPeriod.tasks} - - - Précédente: {comparison.previousPeriod.tasks} - -
-
- - {/* Daily items */} -
-
- Daily items - - {getChangeIcon(comparison.changes.checkboxes)} {formatChange(comparison.changes.checkboxes)} - -
-
- - Actuelle: {comparison.currentPeriod.checkboxes} - - - Précédente: {comparison.previousPeriod.checkboxes} - -
-
- - {/* Total */} -
-
- Total activités - - {getChangeIcon(comparison.changes.total)} {formatChange(comparison.changes.total)} - -
-
- - Actuelle: {comparison.currentPeriod.total} - - - Précédente: {comparison.previousPeriod.total} - -
-
-
- - {/* Insights sur la comparaison */} -
-
💡 Insights comparatifs
-
- {comparison.changes.total > 15 && ( -

🚀 Excellente progression ! Productivité en hausse de {formatChange(comparison.changes.total)}.

- )} - {comparison.changes.total < -15 && ( -

📉 Baisse d'activité de {formatChange(Math.abs(comparison.changes.total))}. Période moins chargée ?

- )} - {Math.abs(comparison.changes.total) <= 15 && ( -

✅ Rythme stable maintenu entre les deux périodes.

- )} - - {comparison.changes.tasks > comparison.changes.checkboxes + 10 && ( -

🎯 Focus accru sur les tâches importantes cette période.

- )} - {comparison.changes.checkboxes > comparison.changes.tasks + 10 && ( -

📝 Activité quotidienne plus intense cette période.

- )} - -

- 📊 Évolution globale: {comparison.currentPeriod.total} activités vs {comparison.previousPeriod.total} la période précédente. -

-
-
-
-
- )} -
- ); -} diff --git a/components/dashboard/VelocityMetrics.tsx b/components/dashboard/VelocityMetrics.tsx deleted file mode 100644 index db60284..0000000 --- a/components/dashboard/VelocityMetrics.tsx +++ /dev/null @@ -1,168 +0,0 @@ -'use client'; - -import type { VelocityMetrics } from '@/services/weekly-summary'; -import { Card, CardHeader, CardContent } from '@/components/ui/Card'; -import { Badge } from '@/components/ui/Badge'; - -interface VelocityMetricsProps { - velocity: VelocityMetrics; -} - -export function VelocityMetrics({ velocity }: VelocityMetricsProps) { - const getTrendIcon = (trend: number) => { - if (trend > 10) return '📈'; - if (trend < -10) return '📉'; - return '➡️'; - }; - - const getTrendColor = (trend: number) => { - if (trend > 10) return 'text-[var(--success)]'; - if (trend < -10) return 'text-[var(--destructive)]'; - return 'text-[var(--muted-foreground)]'; - }; - - const formatTrend = (trend: number) => { - const sign = trend > 0 ? '+' : ''; - return `${sign}${trend.toFixed(1)}%`; - }; - - const maxActivities = Math.max(...velocity.weeklyData.map(w => w.totalActivities)); - - return ( - - -

⚡ Métriques de vélocité

-

- Performance sur les 4 dernières semaines -

-
- - - {/* Métriques principales */} -
-
-
- {velocity.currentWeekTasks} -
-
Tâches cette semaine
-
- -
-
- {velocity.previousWeekTasks} -
-
Semaine précédente
-
- -
-
- {velocity.fourWeekAverage.toFixed(1)} -
-
Moyenne 4 semaines
-
- -
-
- {getTrendIcon(velocity.weeklyTrend)} {formatTrend(velocity.weeklyTrend)} -
-
Tendance
-
-
- - {/* Graphique de tendance simple */} -
-

📊 Tendance sur 4 semaines

-
- {velocity.weeklyData.map((week, index) => { - const height = maxActivities > 0 ? (week.totalActivities / maxActivities) * 100 : 0; - const weekLabel = `S${index + 1}`; - - return ( -
-
-
-
-
{weekLabel}
-
- {week.totalActivities} -
-
- {new Date(week.weekStart).toLocaleDateString('fr-FR', { - day: 'numeric', - month: 'short' - })} -
-
- ); - })} -
-
- - {/* Détails par semaine */} -
-

📈 Détails par semaine

-
- {velocity.weeklyData.map((week, index) => { - const isCurrentWeek = index === velocity.weeklyData.length - 1; - return ( -
-
- - Semaine du {new Date(week.weekStart).toLocaleDateString('fr-FR', { - day: 'numeric', - month: 'short' - })} - - {isCurrentWeek && ( - Actuelle - )} -
-
- - {week.completedTasks} tâches - - - {week.completedCheckboxes} daily - - - Total: {week.totalActivities} - -
-
- ); - })} -
-
- - {/* Insights */} -
-

💡 Insights

-
- {velocity.weeklyTrend > 10 && ( -

🚀 Excellente progression ! Vous êtes {velocity.weeklyTrend.toFixed(1)}% plus productif cette semaine.

- )} - {velocity.weeklyTrend < -10 && ( -

⚠️ Baisse d'activité de {Math.abs(velocity.weeklyTrend).toFixed(1)}%. Peut-être temps de revoir votre planning ?

- )} - {Math.abs(velocity.weeklyTrend) <= 10 && ( -

✅ Rythme stable. Vous maintenez une productivité constante.

- )} -

- 📊 Votre moyenne sur 4 semaines est de {velocity.fourWeekAverage.toFixed(1)} activités par semaine. -

-
-
- - - ); -} - diff --git a/components/dashboard/WeeklySummaryClient.tsx b/components/dashboard/WeeklySummaryClient.tsx deleted file mode 100644 index e9392cf..0000000 --- a/components/dashboard/WeeklySummaryClient.tsx +++ /dev/null @@ -1,448 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { WeeklySummary, WeeklyActivity, PERIOD_OPTIONS, PeriodOption } from '@/services/weekly-summary'; -import { Card, CardHeader, CardContent } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; -import { VelocityMetrics } from './VelocityMetrics'; -import { CategoryBreakdown } from './CategoryBreakdown'; -import { PeriodSelector } from './PeriodSelector'; -import { PDFExportService } from '@/services/pdf-export'; - -interface WeeklySummaryClientProps { - initialSummary: WeeklySummary; -} - -export default function WeeklySummaryClient({ initialSummary }: WeeklySummaryClientProps) { - const [summary, setSummary] = useState(initialSummary); - const [selectedDay, setSelectedDay] = useState(null); - const [isRefreshing, setIsRefreshing] = useState(false); - const [isExportingPDF, setIsExportingPDF] = useState(false); - const [currentPeriod, setCurrentPeriod] = useState(PERIOD_OPTIONS[0]); - const [activeTab, setActiveTab] = useState('all'); - - const handlePeriodChange = async (newPeriod: PeriodOption) => { - setCurrentPeriod(newPeriod); - setIsRefreshing(true); - - try { - // Appel API pour récupérer les données de la nouvelle période - const response = await fetch(`/api/weekly-summary?period=${newPeriod.days}`); - if (response.ok) { - const newSummary = await response.json(); - setSummary(newSummary); - } else { - console.error('Erreur lors du changement de période'); - } - } catch (error) { - console.error('Erreur lors du changement de période:', error); - } finally { - setIsRefreshing(false); - } - }; - - const handleRefresh = async () => { - setIsRefreshing(true); - // Recharger la page pour refaire le fetch côté serveur - window.location.reload(); - }; - - const handleExportPDF = async () => { - setIsExportingPDF(true); - try { - await PDFExportService.exportWeeklySummary(summary); - } catch (error) { - console.error('Erreur lors de l\'export PDF:', error); - alert('Erreur lors de la génération du PDF'); - } finally { - setIsExportingPDF(false); - } - }; - - const formatDate = (date: Date) => { - return new Date(date).toLocaleDateString('fr-FR', { - weekday: 'long', - day: 'numeric', - month: 'long' - }); - }; - - const getActivityIcon = (activity: WeeklyActivity) => { - if (activity.type === 'checkbox') { - return activity.completed ? '✅' : '☐'; - } - return activity.completed ? '🎯' : '📝'; - }; - - const getActivityTypeLabel = (type: 'checkbox' | 'task') => { - return type === 'checkbox' ? 'Daily' : 'Tâche'; - }; - - // Obtenir les catégories disponibles - const availableCategories = Object.keys(summary.categoryBreakdown).filter( - categoryName => summary.categoryBreakdown[categoryName].count > 0 - ); - - // Fonction pour catégoriser une activité - const getActivityCategory = (activity: WeeklyActivity): string => { - // Logique simple pour associer une activité à une catégorie - // En production, cette logique devrait être dans un service - const title = activity.title.toLowerCase(); - - if (title.includes('meeting') || title.includes('réunion') || title.includes('call') || title.includes('standup')) { - return 'Meeting'; - } - if (title.includes('dev') || title.includes('code') || title.includes('bug') || title.includes('fix') || title.includes('feature')) { - return 'Dev'; - } - if (title.includes('admin') || title.includes('email') || title.includes('report') || title.includes('planning')) { - return 'Admin'; - } - if (title.includes('learn') || title.includes('study') || title.includes('formation') || title.includes('tutorial')) { - return 'Learning'; - } - return 'Other'; - }; - - // Filtrer les activités - let filteredActivities = selectedDay - ? summary.activities.filter(a => a.dayName === selectedDay) - : summary.activities; - - // Filtrer par catégorie si ce n'est pas "all" - if (activeTab !== 'all') { - filteredActivities = filteredActivities.filter(activity => - getActivityCategory(activity) === activeTab - ); - } - - return ( -
- {/* Sélecteur de période */} - - - {/* Métriques de vélocité */} - - - {/* Onglets par catégorie */} -
- -
- - {/* Contenu de l'onglet sélectionné */} - {activeTab === 'all' && ( - <> - {/* Répartition par catégorie */} - - - )} - - {/* Vue spécifique par catégorie */} - {activeTab !== 'all' && ( - - -
- {summary.categoryBreakdown[activeTab]?.icon} -
-

{activeTab}

-

- {summary.categoryBreakdown[activeTab]?.count} activités - ({summary.categoryBreakdown[activeTab]?.percentage.toFixed(1)}% de votre temps) -

-
-
-
- - {/* Métriques spécifiques à la catégorie */} -
-
-
- {filteredActivities.length} -
-
Total activités
-
- -
-
- {filteredActivities.filter(a => a.completed).length} -
-
Complétées
-
- -
-
- {filteredActivities.length > 0 - ? Math.round((filteredActivities.filter(a => a.completed).length / filteredActivities.length) * 100) - : 0}% -
-
Taux completion
-
- -
-
- {summary.categoryBreakdown[activeTab]?.percentage.toFixed(1)}% -
-
Du temps total
-
-
-
-
- )} - - {/* Contenu commun à tous les onglets */} - - -
-
-

- {activeTab === 'all' ? '📅 Résumé de la semaine' : `${summary.categoryBreakdown[activeTab]?.icon} Activités ${activeTab}`} -

-

- Du {formatDate(summary.period.start)} au {formatDate(summary.period.end)} - {activeTab !== 'all' && ` • ${filteredActivities.length} activités`} -

-
-
- - -
-
-
- - - {/* Statistiques globales ou spécifiques à la catégorie */} - {activeTab === 'all' ? ( -
-
-
- {summary.stats.completedCheckboxes} -
-
Daily items
-
- sur {summary.stats.totalCheckboxes} ({summary.stats.checkboxCompletionRate.toFixed(0)}%) -
-
- -
-
- {summary.stats.completedTasks} -
-
Tâches
-
- sur {summary.stats.totalTasks} ({summary.stats.taskCompletionRate.toFixed(0)}%) -
-
- -
-
- {summary.stats.completedCheckboxes + summary.stats.completedTasks} -
-
Total complété
-
- sur {summary.stats.totalCheckboxes + summary.stats.totalTasks} -
-
- -
-
- {summary.stats.mostProductiveDay} -
-
Jour le plus productif
-
-
- ) : ( -
-
-
- {filteredActivities.length} -
-
Activités {activeTab}
-
- -
-
- {filteredActivities.filter(a => a.completed).length} -
-
Complétées
-
- -
-
- {filteredActivities.length > 0 - ? Math.round((filteredActivities.filter(a => a.completed).length / filteredActivities.length) * 100) - : 0}% -
-
Taux de réussite
-
- -
-
- {summary.categoryBreakdown[activeTab]?.percentage.toFixed(1)}% -
-
Du temps total
-
-
- )} - - {/* Breakdown par jour */} -
-

- 📊 Répartition par jour - {activeTab !== 'all' && - {activeTab}} -

-
- {summary.stats.dailyBreakdown.map((day) => { - // Pour chaque jour, calculer les activités de la catégorie sélectionnée - const dayActivitiesAll = summary.activities.filter(a => a.dayName === day.dayName); - const dayActivitiesFiltered = activeTab === 'all' - ? dayActivitiesAll - : dayActivitiesAll.filter(activity => getActivityCategory(activity) === activeTab); - - const completedCount = dayActivitiesFiltered.filter(a => a.completed).length; - const totalCount = dayActivitiesFiltered.length; - - return ( - - ); - })} -
- {selectedDay && ( -
- 📍 Filtré sur: {selectedDay} - {activeTab !== 'all' && • Catégorie: {activeTab}} - -
- )} -
- - {/* Timeline des activités */} -
-

- 🕒 Timeline des activités {activeTab !== 'all' && `- ${activeTab}`} - - ({filteredActivities.length} items{activeTab !== 'all' && ` de la catégorie ${activeTab}`}) - -

- - {filteredActivities.length === 0 ? ( -
- {selectedDay ? 'Aucune activité ce jour-là' : 'Aucune activité cette semaine'} -
- ) : ( -
- {filteredActivities.map((activity) => ( -
- - {getActivityIcon(activity)} - - -
-
- - {activity.title} - - - {getActivityTypeLabel(activity.type)} - -
-
- {activity.dayName} • {new Date(activity.createdAt).toLocaleDateString('fr-FR')} - {activity.completedAt && ( - • Complété le {new Date(activity.completedAt).toLocaleDateString('fr-FR')} - )} -
-
-
- ))} -
- )} -
-
-
-
- ); -} diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx index bcce456..8209b15 100644 --- a/components/ui/Header.tsx +++ b/components/ui/Header.tsx @@ -53,7 +53,6 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s { href: '/', label: 'Dashboard' }, { href: '/kanban', label: 'Kanban' }, { href: '/daily', label: 'Daily' }, - { href: '/weekly-summary', label: 'Hebdo' }, { href: '/weekly-manager', label: 'Manager' }, ...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []), { href: '/settings', label: 'Settings' } diff --git a/services/pdf-export.ts b/services/pdf-export.ts deleted file mode 100644 index 955c42f..0000000 --- a/services/pdf-export.ts +++ /dev/null @@ -1,193 +0,0 @@ -import jsPDF from 'jspdf'; -import { WeeklySummary } from './weekly-summary'; - -export class PDFExportService { - /** - * Génère un PDF du résumé hebdomadaire - */ - static async exportWeeklySummary(summary: WeeklySummary): Promise { - const pdf = new jsPDF(); - const pageWidth = pdf.internal.pageSize.getWidth(); - const margin = 20; - let yPosition = margin + 10; - - // Header avec logo/titre - pdf.setFontSize(24); - pdf.setFont('helvetica', 'bold'); - pdf.text('📊 RÉSUMÉ HEBDOMADAIRE', pageWidth / 2, yPosition, { align: 'center' }); - - yPosition += 15; - pdf.setFontSize(12); - pdf.setFont('helvetica', 'normal'); - const dateRange = `Du ${this.formatDate(summary.period.start)} au ${this.formatDate(summary.period.end)}`; - pdf.text(dateRange, pageWidth / 2, yPosition, { align: 'center' }); - - yPosition += 20; - - // Section métriques principales - pdf.setFontSize(16); - pdf.setFont('helvetica', 'bold'); - pdf.text('🎯 MÉTRIQUES PRINCIPALES', margin, yPosition); - yPosition += 15; - - pdf.setFontSize(11); - pdf.setFont('helvetica', 'normal'); - - // Statistiques en deux colonnes - const statsLeft = [ - `Tâches complétées: ${summary.stats.completedTasks}/${summary.stats.totalTasks}`, - `Taux de réussite tâches: ${summary.stats.taskCompletionRate.toFixed(1)}%`, - `Daily items complétés: ${summary.stats.completedCheckboxes}/${summary.stats.totalCheckboxes}`, - `Taux de réussite daily: ${summary.stats.checkboxCompletionRate.toFixed(1)}%` - ]; - - const statsRight = [ - `Vélocité actuelle: ${summary.velocity.currentWeekTasks} tâches`, - `Semaine précédente: ${summary.velocity.previousWeekTasks} tâches`, - `Moyenne 4 semaines: ${summary.velocity.fourWeekAverage.toFixed(1)}`, - `Tendance: ${this.formatTrend(summary.velocity.weeklyTrend)}` - ]; - - // Colonne gauche - statsLeft.forEach((stat, index) => { - pdf.text(`• ${stat}`, margin, yPosition + (index * 8)); - }); - - // Colonne droite - statsRight.forEach((stat, index) => { - pdf.text(`• ${stat}`, pageWidth / 2 + 10, yPosition + (index * 8)); - }); - - yPosition += 40; - - // Section jour le plus productif - pdf.setFontSize(14); - pdf.setFont('helvetica', 'bold'); - pdf.text('⭐ INSIGHTS', margin, yPosition); - yPosition += 12; - - pdf.setFontSize(11); - pdf.setFont('helvetica', 'normal'); - pdf.text(`• Jour le plus productif: ${summary.stats.mostProductiveDay}`, margin + 5, yPosition); - yPosition += 8; - - // Tendance insight - let trendInsight = ''; - if (summary.velocity.weeklyTrend > 10) { - trendInsight = `• Excellente progression ! Amélioration de ${summary.velocity.weeklyTrend.toFixed(1)}% cette semaine.`; - } else if (summary.velocity.weeklyTrend < -10) { - trendInsight = `• Baisse d'activité de ${Math.abs(summary.velocity.weeklyTrend).toFixed(1)}%. Temps de revoir le planning ?`; - } else { - trendInsight = '• Rythme stable maintenu cette semaine.'; - } - pdf.text(trendInsight, margin + 5, yPosition); - yPosition += 12; - - // Section breakdown par jour - pdf.setFontSize(14); - pdf.setFont('helvetica', 'bold'); - pdf.text('📅 RÉPARTITION PAR JOUR', margin, yPosition); - yPosition += 12; - - pdf.setFontSize(10); - pdf.setFont('helvetica', 'normal'); - - // Headers du tableau - const colWidth = (pageWidth - 2 * margin) / 4; - pdf.text('Jour', margin, yPosition); - pdf.text('Tâches', margin + colWidth, yPosition); - pdf.text('Daily Items', margin + 2 * colWidth, yPosition); - pdf.text('Total', margin + 3 * colWidth, yPosition); - yPosition += 8; - - // Ligne de séparation - pdf.line(margin, yPosition - 2, pageWidth - margin, yPosition - 2); - - // Données du tableau - summary.stats.dailyBreakdown.forEach((day) => { - pdf.text(day.dayName, margin, yPosition); - pdf.text(`${day.completedTasks}/${day.tasks}`, margin + colWidth, yPosition); - pdf.text(`${day.completedCheckboxes}/${day.checkboxes}`, margin + 2 * colWidth, yPosition); - pdf.text(`${day.completedTasks + day.completedCheckboxes}`, margin + 3 * colWidth, yPosition); - yPosition += 6; - }); - - yPosition += 10; - - // Section principales réalisations - pdf.setFontSize(14); - pdf.setFont('helvetica', 'bold'); - pdf.text('✅ PRINCIPALES RÉALISATIONS', margin, yPosition); - yPosition += 12; - - pdf.setFontSize(10); - pdf.setFont('helvetica', 'normal'); - - const completedActivities = summary.activities - .filter(a => a.completed) - .slice(0, 8); // Top 8 réalisations - - if (completedActivities.length === 0) { - pdf.text('Aucune réalisation complétée cette semaine.', margin + 5, yPosition); - yPosition += 8; - } else { - completedActivities.forEach((activity) => { - const truncatedTitle = activity.title.length > 60 - ? activity.title.substring(0, 57) + '...' - : activity.title; - - const icon = activity.type === 'task' ? '🎯' : '✅'; - pdf.text(`${icon} ${truncatedTitle}`, margin + 5, yPosition); - yPosition += 6; - - // Nouvelle page si nécessaire - if (yPosition > pdf.internal.pageSize.getHeight() - 30) { - pdf.addPage(); - yPosition = margin + 10; - } - }); - } - - // Footer - const totalPages = pdf.internal.getNumberOfPages(); - for (let i = 1; i <= totalPages; i++) { - pdf.setPage(i); - pdf.setFontSize(8); - pdf.setFont('helvetica', 'normal'); - const footerText = `TowerControl - Généré le ${new Date().toLocaleDateString('fr-FR')} - Page ${i}/${totalPages}`; - pdf.text(footerText, pageWidth / 2, pdf.internal.pageSize.getHeight() - 10, { align: 'center' }); - } - - // Télécharger le PDF - const fileName = `weekly-summary-${this.formatDateForFilename(summary.period.start)}_${this.formatDateForFilename(summary.period.end)}.pdf`; - pdf.save(fileName); - } - - /** - * Formate une date pour l'affichage - */ - private static formatDate(date: Date): string { - return new Date(date).toLocaleDateString('fr-FR', { - weekday: 'long', - day: 'numeric', - month: 'long', - year: 'numeric' - }); - } - - /** - * Formate une date pour le nom de fichier - */ - private static formatDateForFilename(date: Date): string { - return new Date(date).toISOString().split('T')[0]; - } - - /** - * Formate le pourcentage de tendance - */ - private static formatTrend(trend: number): string { - const sign = trend > 0 ? '+' : ''; - return `${sign}${trend.toFixed(1)}%`; - } -} - diff --git a/services/weekly-summary.ts b/services/weekly-summary.ts deleted file mode 100644 index 8e7d64d..0000000 --- a/services/weekly-summary.ts +++ /dev/null @@ -1,438 +0,0 @@ -import { prisma } from './database'; -import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; -import { TaskCategorizationService } from './task-categorization'; - -export interface DailyItem { - id: string; - text: string; - isChecked: boolean; - createdAt: Date; - updatedAt: Date; - date: Date; -} - -export interface WeeklyStats { - totalCheckboxes: number; - completedCheckboxes: number; - totalTasks: number; - completedTasks: number; - checkboxCompletionRate: number; - taskCompletionRate: number; - mostProductiveDay: string; - dailyBreakdown: Array<{ - date: string; - dayName: string; - checkboxes: number; - completedCheckboxes: number; - tasks: number; - completedTasks: number; - }>; -} - -export interface WeeklyActivity { - id: string; - type: 'checkbox' | 'task'; - title: string; - completed: boolean; - completedAt?: Date; - createdAt: Date; - date: string; - dayName: string; -} - -export interface VelocityMetrics { - currentWeekTasks: number; - previousWeekTasks: number; - weeklyTrend: number; // Pourcentage d'amélioration/détérioration - fourWeekAverage: number; - weeklyData: Array<{ - weekStart: Date; - weekEnd: Date; - completedTasks: number; - completedCheckboxes: number; - totalActivities: number; - }>; -} - -export interface PeriodComparison { - currentPeriod: { - tasks: number; - checkboxes: number; - total: number; - }; - previousPeriod: { - tasks: number; - checkboxes: number; - total: number; - }; - changes: { - tasks: number; // pourcentage de changement - checkboxes: number; - total: number; - }; -} - -export interface WeeklySummary { - stats: WeeklyStats; - activities: WeeklyActivity[]; - velocity: VelocityMetrics; - categoryBreakdown: { [categoryName: string]: { count: number; percentage: number; color: string; icon: string } }; - periodComparison: PeriodComparison | null; - period: { - start: Date; - end: Date; - }; -} - -export interface PeriodOption { - label: string; - days: number; - key: string; -} - -export const PERIOD_OPTIONS: PeriodOption[] = [ - { label: 'Dernière semaine', days: 7, key: 'week' }, - { label: 'Dernières 2 semaines', days: 14, key: '2weeks' }, - { label: 'Dernier mois', days: 30, key: 'month' } -]; - -export class WeeklySummaryService { - /** - * Récupère le résumé complet de la semaine écoulée - */ - static async getWeeklySummary(periodDays: number = 7): Promise { - const now = new Date(); - const startOfPeriod = new Date(now); - startOfPeriod.setDate(now.getDate() - periodDays); - startOfPeriod.setHours(0, 0, 0, 0); - - const endOfPeriod = new Date(now); - endOfPeriod.setHours(23, 59, 59, 999); - - console.log(`📊 Génération du résumé (${periodDays} jours) du ${startOfPeriod.toLocaleDateString()} au ${endOfPeriod.toLocaleDateString()}`); - - const [checkboxes, tasks, velocity] = await Promise.all([ - this.getWeeklyCheckboxes(startOfPeriod, endOfPeriod), - this.getWeeklyTasks(startOfPeriod, endOfPeriod), - this.calculateVelocityMetrics(endOfPeriod) - ]); - - const stats = this.calculateStats(checkboxes, tasks, startOfPeriod, endOfPeriod); - const activities = this.mergeActivities(checkboxes, tasks); - const categoryBreakdown = this.analyzeCategorization(checkboxes, tasks); - - // Calculer la comparaison avec la période précédente - const periodComparison = await this.calculatePeriodComparison(startOfPeriod, endOfPeriod, periodDays); - - return { - stats, - activities, - velocity, - categoryBreakdown, - periodComparison, - period: { - start: startOfPeriod, - end: endOfPeriod - } - }; - } - - /** - * Récupère les checkboxes des 7 derniers jours - */ - private static async getWeeklyCheckboxes(startDate: Date, endDate: Date): Promise { - const items = await prisma.dailyCheckbox.findMany({ - where: { - date: { - gte: startDate, - lte: endDate - } - }, - orderBy: [ - { date: 'desc' }, - { createdAt: 'desc' } - ] - }); - - return items.map(item => ({ - id: item.id, - text: item.text, - isChecked: item.isChecked, - createdAt: item.createdAt, - updatedAt: item.updatedAt, - date: item.date - })); - } - - /** - * Récupère les tâches des 7 derniers jours (créées ou modifiées) - */ - private static async getWeeklyTasks(startDate: Date, endDate: Date): Promise { - const tasks = await prisma.task.findMany({ - where: { - OR: [ - { - createdAt: { - gte: startDate, - lte: endDate - } - }, - { - updatedAt: { - gte: startDate, - lte: endDate - } - } - ] - }, - orderBy: { - updatedAt: 'desc' - } - }); - - return tasks.map(task => ({ - id: task.id, - title: task.title, - description: task.description || '', - status: task.status as TaskStatus, - priority: task.priority as TaskPriority, - source: task.source as TaskSource, - sourceId: task.sourceId || undefined, - createdAt: task.createdAt, - updatedAt: task.updatedAt, - dueDate: task.dueDate || undefined, - completedAt: task.completedAt || undefined, - jiraProject: task.jiraProject || undefined, - jiraKey: task.jiraKey || undefined, - jiraType: task.jiraType || undefined, - assignee: task.assignee || undefined, - tags: [] // Les tags sont dans une relation séparée, on les laisse vides pour l'instant - })); - } - - /** - * Calcule les statistiques de la semaine - */ - private static calculateStats( - checkboxes: DailyItem[], - tasks: Task[], - startDate: Date, - endDate: Date - ): WeeklyStats { - const completedCheckboxes = checkboxes.filter(c => c.isChecked); - const completedTasks = tasks.filter(t => t.status === 'done'); - - // Créer un breakdown par jour - const dailyBreakdown = []; - const current = new Date(startDate); - - while (current <= endDate) { - const dayCheckboxes = checkboxes.filter(c => - c.date.toISOString().split('T')[0] === current.toISOString().split('T')[0] - ); - const dayCompletedCheckboxes = dayCheckboxes.filter(c => c.isChecked); - - // Pour les tâches, on compte celles modifiées ce jour-là - const dayTasks = tasks.filter(t => - t.updatedAt.toISOString().split('T')[0] === current.toISOString().split('T')[0] || - t.createdAt.toISOString().split('T')[0] === current.toISOString().split('T')[0] - ); - const dayCompletedTasks = dayTasks.filter(t => t.status === 'done'); - - dailyBreakdown.push({ - date: current.toISOString().split('T')[0], - dayName: current.toLocaleDateString('fr-FR', { weekday: 'long' }), - checkboxes: dayCheckboxes.length, - completedCheckboxes: dayCompletedCheckboxes.length, - tasks: dayTasks.length, - completedTasks: dayCompletedTasks.length - }); - - current.setDate(current.getDate() + 1); - } - - // Trouver le jour le plus productif - const mostProductiveDay = dailyBreakdown.reduce((max, day) => { - const dayScore = day.completedCheckboxes + day.completedTasks; - const maxScore = max.completedCheckboxes + max.completedTasks; - return dayScore > maxScore ? day : max; - }, dailyBreakdown[0]); - - return { - totalCheckboxes: checkboxes.length, - completedCheckboxes: completedCheckboxes.length, - totalTasks: tasks.length, - completedTasks: completedTasks.length, - checkboxCompletionRate: checkboxes.length > 0 ? (completedCheckboxes.length / checkboxes.length) * 100 : 0, - taskCompletionRate: tasks.length > 0 ? (completedTasks.length / tasks.length) * 100 : 0, - mostProductiveDay: mostProductiveDay.dayName, - dailyBreakdown - }; - } - - /** - * Calcule les métriques de vélocité sur 4 semaines - */ - private static async calculateVelocityMetrics(currentEndDate: Date): Promise { - const weeks = []; - const currentDate = new Date(currentEndDate); - - // Générer les 4 dernières semaines - for (let i = 0; i < 4; i++) { - const weekEnd = new Date(currentDate); - weekEnd.setDate(weekEnd.getDate() - (i * 7)); - weekEnd.setHours(23, 59, 59, 999); - - const weekStart = new Date(weekEnd); - weekStart.setDate(weekEnd.getDate() - 6); - weekStart.setHours(0, 0, 0, 0); - - const [weekCheckboxes, weekTasks] = await Promise.all([ - this.getWeeklyCheckboxes(weekStart, weekEnd), - this.getWeeklyTasks(weekStart, weekEnd) - ]); - - const completedTasks = weekTasks.filter(t => t.status === 'done').length; - const completedCheckboxes = weekCheckboxes.filter(c => c.isChecked).length; - - weeks.push({ - weekStart, - weekEnd, - completedTasks, - completedCheckboxes, - totalActivities: completedTasks + completedCheckboxes - }); - } - - // Calculer les métriques - const currentWeek = weeks[0]; - const previousWeek = weeks[1]; - const fourWeekAverage = weeks.reduce((sum, week) => sum + week.totalActivities, 0) / weeks.length; - - let weeklyTrend = 0; - if (previousWeek.totalActivities > 0) { - weeklyTrend = ((currentWeek.totalActivities - previousWeek.totalActivities) / previousWeek.totalActivities) * 100; - } else if (currentWeek.totalActivities > 0) { - weeklyTrend = 100; // 100% d'amélioration si on passe de 0 à quelque chose - } - - return { - currentWeekTasks: currentWeek.completedTasks, - previousWeekTasks: previousWeek.completedTasks, - weeklyTrend, - fourWeekAverage, - weeklyData: weeks.reverse() // Plus ancien en premier pour l'affichage du graphique - }; - } - - /** - * Calcule la comparaison avec la période précédente - */ - private static async calculatePeriodComparison( - currentStart: Date, - currentEnd: Date, - periodDays: number - ): Promise { - // Période précédente - const previousEnd = new Date(currentStart); - previousEnd.setHours(23, 59, 59, 999); - - const previousStart = new Date(previousEnd); - previousStart.setDate(previousEnd.getDate() - periodDays); - previousStart.setHours(0, 0, 0, 0); - - const [currentCheckboxes, currentTasks, previousCheckboxes, previousTasks] = await Promise.all([ - this.getWeeklyCheckboxes(currentStart, currentEnd), - this.getWeeklyTasks(currentStart, currentEnd), - this.getWeeklyCheckboxes(previousStart, previousEnd), - this.getWeeklyTasks(previousStart, previousEnd) - ]); - - const currentCompletedTasks = currentTasks.filter(t => t.status === 'done').length; - const currentCompletedCheckboxes = currentCheckboxes.filter(c => c.isChecked).length; - const currentTotal = currentCompletedTasks + currentCompletedCheckboxes; - - const previousCompletedTasks = previousTasks.filter(t => t.status === 'done').length; - const previousCompletedCheckboxes = previousCheckboxes.filter(c => c.isChecked).length; - const previousTotal = previousCompletedTasks + previousCompletedCheckboxes; - - const calculateChange = (current: number, previous: number): number => { - if (previous === 0) return current > 0 ? 100 : 0; - return ((current - previous) / previous) * 100; - }; - - return { - currentPeriod: { - tasks: currentCompletedTasks, - checkboxes: currentCompletedCheckboxes, - total: currentTotal - }, - previousPeriod: { - tasks: previousCompletedTasks, - checkboxes: previousCompletedCheckboxes, - total: previousTotal - }, - changes: { - tasks: calculateChange(currentCompletedTasks, previousCompletedTasks), - checkboxes: calculateChange(currentCompletedCheckboxes, previousCompletedCheckboxes), - total: calculateChange(currentTotal, previousTotal) - } - }; - } - - /** - * Analyse la catégorisation des activités - */ - private static analyzeCategorization(checkboxes: DailyItem[], tasks: Task[]): { [categoryName: string]: { count: number; percentage: number; color: string; icon: string } } { - const allActivities = [ - ...checkboxes.map(c => ({ title: c.text, description: '' })), - ...tasks.map(t => ({ title: t.title, description: t.description || '' })) - ]; - - return TaskCategorizationService.analyzeActivitiesByCategory(allActivities); - } - - /** - * Fusionne les activités (checkboxes + tâches) en une timeline - */ - private static mergeActivities(checkboxes: DailyItem[], tasks: Task[]): WeeklyActivity[] { - const activities: WeeklyActivity[] = []; - - // Ajouter les checkboxes - checkboxes.forEach(checkbox => { - activities.push({ - id: `checkbox-${checkbox.id}`, - type: 'checkbox', - title: checkbox.text, - completed: checkbox.isChecked, - completedAt: checkbox.isChecked ? checkbox.updatedAt : undefined, - createdAt: checkbox.createdAt, - date: checkbox.date.toISOString().split('T')[0], - dayName: checkbox.date.toLocaleDateString('fr-FR', { weekday: 'long' }) - }); - }); - - // Ajouter les tâches - tasks.forEach(task => { - const date = task.updatedAt.toISOString().split('T')[0]; - const dateObj = new Date(date + 'T00:00:00'); - activities.push({ - id: `task-${task.id}`, - type: 'task', - title: task.title, - completed: task.status === 'done', - completedAt: task.status === 'done' ? task.updatedAt : undefined, - createdAt: task.createdAt, - date: date, - dayName: dateObj.toLocaleDateString('fr-FR', { weekday: 'long' }) - }); - }); - - // Trier par date (plus récent en premier) - return activities.sort((a, b) => { - const dateA = a.completedAt || a.createdAt; - const dateB = b.completedAt || b.createdAt; - return dateB.getTime() - dateA.getTime(); - }); - } -} diff --git a/src/app/api/weekly-summary/route.ts b/src/app/api/weekly-summary/route.ts deleted file mode 100644 index 5270e8a..0000000 --- a/src/app/api/weekly-summary/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { WeeklySummaryService } from '@/services/weekly-summary'; - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const periodParam = searchParams.get('period'); - - // Valider le paramètre de période - let periodDays = 7; // Défaut: 7 jours - if (periodParam) { - const parsedPeriod = parseInt(periodParam, 10); - if (!isNaN(parsedPeriod) && parsedPeriod > 0 && parsedPeriod <= 365) { - periodDays = parsedPeriod; - } - } - - console.log(`📊 API weekly-summary appelée avec période: ${periodDays} jours`); - - const summary = await WeeklySummaryService.getWeeklySummary(periodDays); - - return NextResponse.json(summary, { - headers: { - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0' - } - }); - } catch (error) { - console.error('Erreur lors de la génération du résumé hebdomadaire:', error); - return NextResponse.json( - { error: 'Erreur lors de la génération du résumé' }, - { status: 500 } - ); - } -} diff --git a/src/app/weekly-summary/page.tsx b/src/app/weekly-summary/page.tsx deleted file mode 100644 index 0d1102c..0000000 --- a/src/app/weekly-summary/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Header } from '@/components/ui/Header'; -import WeeklySummaryClient from '@/components/dashboard/WeeklySummaryClient'; -import { WeeklySummaryService } from '@/services/weekly-summary'; - -export default async function WeeklySummaryPage() { - // Récupération côté serveur - const summary = await WeeklySummaryService.getWeeklySummary(); - - return ( -
-
- -
-
- -
-
-
- ); -}