diff --git a/components/dashboard/WeeklySummaryClient.tsx b/components/dashboard/WeeklySummaryClient.tsx new file mode 100644 index 0000000..79fff58 --- /dev/null +++ b/components/dashboard/WeeklySummaryClient.tsx @@ -0,0 +1,200 @@ +'use client'; + +import { useState } from 'react'; +import { WeeklySummary, WeeklyActivity } from '@/services/weekly-summary'; +import { Card, CardHeader, CardContent } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; + +interface WeeklySummaryClientProps { + initialSummary: WeeklySummary; +} + +export default function WeeklySummaryClient({ initialSummary }: WeeklySummaryClientProps) { + const [summary] = useState(initialSummary); + const [selectedDay, setSelectedDay] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(false); + + const handleRefresh = async () => { + setIsRefreshing(true); + // Recharger la page pour refaire le fetch côté serveur + window.location.reload(); + }; + + 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'; + }; + + const filteredActivities = selectedDay + ? summary.activities.filter(a => a.dayName === selectedDay) + : summary.activities; + + return ( + + +
+
+

📅 Résumé de la semaine

+

+ Du {formatDate(summary.period.start)} au {formatDate(summary.period.end)} +

+
+ +
+
+ + + {/* Statistiques globales */} +
+
+
+ {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
+
+
+ + {/* Breakdown par jour */} +
+

📊 Répartition par jour

+
+ {summary.stats.dailyBreakdown.map((day) => ( + + ))} +
+ {selectedDay && ( +
+ 📍 Filtré sur: {selectedDay} + +
+ )} +
+ + {/* Timeline des activités */} +
+

+ 🕒 Timeline des activités + + ({filteredActivities.length} items) + +

+ + {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 bef99a6..e9e87f1 100644 --- a/components/ui/Header.tsx +++ b/components/ui/Header.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useTheme } from '@/contexts/ThemeContext'; import { useJiraConfig } from '@/contexts/JiraConfigContext'; import { usePathname } from 'next/navigation'; @@ -51,6 +53,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s { href: '/', label: 'Dashboard' }, { href: '/kanban', label: 'Kanban' }, { href: '/daily', label: 'Daily' }, + { href: '/weekly-summary', label: 'Résumé' }, { href: '/tags', label: 'Tags' }, ...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []), { href: '/settings', label: 'Settings' } diff --git a/services/weekly-summary.ts b/services/weekly-summary.ts new file mode 100644 index 0000000..a3fa4bb --- /dev/null +++ b/services/weekly-summary.ts @@ -0,0 +1,260 @@ +import { prisma } from './database'; +import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; + +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 WeeklySummary { + stats: WeeklyStats; + activities: WeeklyActivity[]; + period: { + start: Date; + end: Date; + }; +} + +export class WeeklySummaryService { + /** + * Récupère le résumé complet de la semaine écoulée + */ + static async getWeeklySummary(): Promise { + const now = new Date(); + const startOfWeek = new Date(now); + startOfWeek.setDate(now.getDate() - 7); + startOfWeek.setHours(0, 0, 0, 0); + + const endOfWeek = new Date(now); + endOfWeek.setHours(23, 59, 59, 999); + + console.log(`📊 Génération du résumé hebdomadaire du ${startOfWeek.toLocaleDateString()} au ${endOfWeek.toLocaleDateString()}`); + + const [checkboxes, tasks] = await Promise.all([ + this.getWeeklyCheckboxes(startOfWeek, endOfWeek), + this.getWeeklyTasks(startOfWeek, endOfWeek) + ]); + + const stats = this.calculateStats(checkboxes, tasks, startOfWeek, endOfWeek); + const activities = this.mergeActivities(checkboxes, tasks); + + return { + stats, + activities, + period: { + start: startOfWeek, + end: endOfWeek + } + }; + } + + /** + * 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 + }; + } + + /** + * 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/weekly-summary/page.tsx b/src/app/weekly-summary/page.tsx new file mode 100644 index 0000000..0d1102c --- /dev/null +++ b/src/app/weekly-summary/page.tsx @@ -0,0 +1,20 @@ +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 ( +
+
+ +
+
+ +
+
+
+ ); +}