From 8340008839d6f5f861cf409677c51e9602ce186d Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Tue, 11 Nov 2025 08:46:19 +0100 Subject: [PATCH] feat(DailyPage, DailyService, Calendar): enhance task deadline management and UI integration - Implemented user authentication in the daily dates API route to ensure secure access. - Added functionality to retrieve task deadlines and associated tasks, improving task management capabilities. - Updated DailyPageClient to display tasks with deadlines in the calendar view, enhancing user experience. - Enhanced Calendar component to visually indicate deadline dates, providing clearer task management context. --- src/app/api/daily/dates/route.ts | 9 +- src/app/api/daily/deadline-tasks/route.ts | 42 +++++++ src/app/api/daily/deadlines/route.ts | 30 +++++ src/app/daily/DailyPageClient.tsx | 143 +++++++++++++++++++++- src/app/daily/page.tsx | 39 +++--- src/clients/daily-client.ts | 23 ++++ src/components/ui/Calendar.tsx | 46 +++++-- src/services/task-management/daily.ts | 132 ++++++++++++++++++++ 8 files changed, 435 insertions(+), 29 deletions(-) create mode 100644 src/app/api/daily/deadline-tasks/route.ts create mode 100644 src/app/api/daily/deadlines/route.ts diff --git a/src/app/api/daily/dates/route.ts b/src/app/api/daily/dates/route.ts index 33a5f19..c06b40c 100644 --- a/src/app/api/daily/dates/route.ts +++ b/src/app/api/daily/dates/route.ts @@ -1,5 +1,7 @@ import { NextResponse } from 'next/server'; import { dailyService } from '@/services/task-management/daily'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; /** * API route pour récupérer toutes les dates avec des dailies @@ -7,7 +9,12 @@ import { dailyService } from '@/services/task-management/daily'; */ export async function GET() { try { - const dates = await dailyService.getDailyDates(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + + const dates = await dailyService.getDailyDates(session.user.id); return NextResponse.json({ dates }); } catch (error) { console.error('Erreur lors de la récupération des dates:', error); diff --git a/src/app/api/daily/deadline-tasks/route.ts b/src/app/api/daily/deadline-tasks/route.ts new file mode 100644 index 0000000..fc7dee4 --- /dev/null +++ b/src/app/api/daily/deadline-tasks/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from 'next/server'; +import { dailyService } from '@/services/task-management/daily'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; +import { parseDate, isValidAPIDate } from '@/lib/date-utils'; + +/** + * API route pour récupérer les tâches avec deadline pour une date donnée + * GET /api/daily/deadline-tasks?date=YYYY-MM-DD + */ +export async function GET(request: Request) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const dateStr = searchParams.get('date'); + + if (!dateStr || !isValidAPIDate(dateStr)) { + return NextResponse.json( + { error: 'Date invalide. Format attendu: YYYY-MM-DD' }, + { status: 400 } + ); + } + + const date = parseDate(dateStr); + const tasks = await dailyService.getTasksByDeadlineDate( + session.user.id, + date + ); + + return NextResponse.json({ tasks }); + } catch (error) { + console.error('Erreur lors de la récupération des tâches:', error); + return NextResponse.json( + { error: 'Erreur interne du serveur' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/daily/deadlines/route.ts b/src/app/api/daily/deadlines/route.ts new file mode 100644 index 0000000..ee5effa --- /dev/null +++ b/src/app/api/daily/deadlines/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from 'next/server'; +import { dailyService } from '@/services/task-management/daily'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; + +/** + * API route pour récupérer toutes les dates de fin des tâches avec leurs noms + * GET /api/daily/deadlines + * Retourne un objet { dates: Record } où chaque clé est une date (YYYY-MM-DD) + * et la valeur est un tableau de noms de tâches + */ +export async function GET() { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + + const deadlineDates = await dailyService.getTaskDeadlineDates( + session.user.id + ); + return NextResponse.json({ dates: deadlineDates }); + } catch (error) { + console.error('Erreur lors de la récupération des dates de fin:', error); + return NextResponse.json( + { error: 'Erreur interne du serveur' }, + { status: 500 } + ); + } +} diff --git a/src/app/daily/DailyPageClient.tsx b/src/app/daily/DailyPageClient.tsx index 2c1adb2..e300d56 100644 --- a/src/app/daily/DailyPageClient.tsx +++ b/src/app/daily/DailyPageClient.tsx @@ -3,10 +3,12 @@ import { useState, useEffect } from 'react'; import React from 'react'; import { useDaily } from '@/hooks/useDaily'; -import { DailyView, DailyCheckboxType, DailyCheckbox } from '@/lib/types'; +import { DailyView, DailyCheckboxType, DailyCheckbox, Task } from '@/lib/types'; import { DeadlineMetrics } from '@/services/analytics/deadline-analytics'; import { Button } from '@/components/ui/Button'; -import { Card } from '@/components/ui/Card'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; +import { TaskCard } from '@/components/ui/TaskCard'; +import { useTags } from '@/hooks/useTags'; import { Calendar } from '@/components/ui/Calendar'; import { AlertBanner, AlertItem } from '@/components/ui/AlertBanner'; import { DailySection } from '@/components/daily/DailySection'; @@ -18,6 +20,7 @@ import { formatDateLong, isToday, generateDateTitle, + formatDateForAPI, } from '@/lib/date-utils'; import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts'; import { Emoji } from '@/components/ui/Emoji'; @@ -25,6 +28,7 @@ import { Emoji } from '@/components/ui/Emoji'; interface DailyPageClientProps { initialDailyView?: DailyView; initialDailyDates?: string[]; + initialDeadlineDates?: Record; // Date -> Array de noms de tâches initialDate?: Date; initialDeadlineMetrics?: DeadlineMetrics | null; initialPendingTasks?: DailyCheckbox[]; @@ -33,10 +37,12 @@ interface DailyPageClientProps { export function DailyPageClient({ initialDailyView, initialDailyDates = [], + initialDeadlineDates = {}, initialDate, initialDeadlineMetrics, initialPendingTasks = [], }: DailyPageClientProps = {}) { + const { tags: availableTags } = useTags(); const { dailyView, loading, @@ -60,6 +66,10 @@ export function DailyPageClient({ } = useDaily(initialDate, initialDailyView); const [dailyDates, setDailyDates] = useState(initialDailyDates); + const [deadlineDates, setDeadlineDates] = + useState>(initialDeadlineDates); + const [deadlineTasks, setDeadlineTasks] = useState([]); + const [loadingDeadlineTasks, setLoadingDeadlineTasks] = useState(false); const [pendingRefreshTrigger, setPendingRefreshTrigger] = useState(0); // Fonction pour rafraîchir la liste des dates avec des dailies @@ -84,6 +94,18 @@ export function DailyPageClient({ } }, [initialDailyDates.length]); + // Charger les dates de fin pour le calendrier (seulement si pas de données SSR) + useEffect(() => { + if (Object.keys(initialDeadlineDates).length === 0) { + import('@/clients/daily-client') + .then(({ dailyClient }) => { + return dailyClient.getDeadlineDates(); + }) + .then(setDeadlineDates) + .catch(console.error); + } + }, [initialDeadlineDates]); + const handleAddTodayCheckbox = async ( text: string, type: DailyCheckboxType @@ -163,6 +185,29 @@ export function DailyPageClient({ return formatDateLong(currentDate); }; + // Charger les tâches complètes pour la date sélectionnée + useEffect(() => { + const loadDeadlineTasks = async () => { + const dateKey = formatDateForAPI(currentDate); + if (deadlineDates[dateKey] && deadlineDates[dateKey].length > 0) { + setLoadingDeadlineTasks(true); + try { + const tasks = await dailyClient.getDeadlineTasksForDate(currentDate); + setDeadlineTasks(tasks); + } catch (error) { + console.error('Erreur lors du chargement des tâches:', error); + setDeadlineTasks([]); + } finally { + setLoadingDeadlineTasks(false); + } + } else { + setDeadlineTasks([]); + } + }; + + loadDeadlineTasks(); + }, [currentDate, deadlineDates]); + const isTodayDate = () => { return isToday(currentDate); }; @@ -351,9 +396,55 @@ export function DailyPageClient({ currentDate={currentDate} onDateSelect={handleDateSelect} markedDates={dailyDates} + deadlineDates={deadlineDates} showTodayButton={true} showLegend={true} /> + + {/* Section des tâches avec deadline pour la date sélectionnée - Mobile */} + {deadlineTasks.length > 0 && ( + + + +
+ Tâches à terminer +
+
+ + {loadingDeadlineTasks ? ( +
+ Chargement... +
+ ) : ( +
+ {deadlineTasks.map((task) => ( + { + window.location.href = `/kanban?taskId=${task.id}`; + }} + /> + ))} +
+ )} +
+
+ )} )} @@ -362,14 +453,60 @@ export function DailyPageClient({
{/* Calendrier - Desktop */} -
+
+ + {/* Section des tâches avec deadline pour la date sélectionnée */} + {deadlineTasks.length > 0 && ( + + + +
+ Tâches à terminer +
+
+ + {loadingDeadlineTasks ? ( +
+ Chargement... +
+ ) : ( +
+ {deadlineTasks.map((task) => ( + { + window.location.href = `/kanban?taskId=${task.id}`; + }} + /> + ))} +
+ )} +
+
+ )}
{/* Sections daily - Desktop */} diff --git a/src/app/daily/page.tsx b/src/app/daily/page.tsx index 59dbb94..36b6c64 100644 --- a/src/app/daily/page.tsx +++ b/src/app/daily/page.tsx @@ -37,27 +37,34 @@ export default async function DailyPage() { const today = getToday(); try { - const [dailyView, dailyDates, deadlineMetrics, pendingTasks] = - await Promise.all([ - dailyService.getDailyView(today, session.user.id), - dailyService.getDailyDates(session.user.id), - DeadlineAnalyticsService.getDeadlineMetrics(session.user.id).catch( - () => null - ), // Graceful fallback - dailyService - .getPendingCheckboxes({ - maxDays: 7, - excludeToday: true, - limit: 50, - userId: session.user.id, - }) - .catch(() => []), // Graceful fallback - ]); + const [ + dailyView, + dailyDates, + deadlineDatesMap, + deadlineMetrics, + pendingTasks, + ] = await Promise.all([ + dailyService.getDailyView(today, session.user.id), + dailyService.getDailyDates(session.user.id), + dailyService.getTaskDeadlineDates(session.user.id).catch(() => ({})), // Graceful fallback + DeadlineAnalyticsService.getDeadlineMetrics(session.user.id).catch( + () => null + ), // Graceful fallback + dailyService + .getPendingCheckboxes({ + maxDays: 7, + excludeToday: true, + limit: 50, + userId: session.user.id, + }) + .catch(() => []), // Graceful fallback + ]); return ( où chaque clé est une date (YYYY-MM-DD) + * et la valeur est un tableau de noms de tâches + */ + async getDeadlineDates(): Promise> { + const response = await httpClient.get<{ dates: Record }>( + '/daily/deadlines' + ); + return response.dates; + } + + /** + * Récupère les tâches avec deadline pour une date donnée + */ + async getDeadlineTasksForDate(date: Date): Promise { + const dateStr = this.formatDateForAPI(date); + const response = await httpClient.get<{ + tasks: Task[]; + }>(`/daily/deadline-tasks?date=${dateStr}`); + return response.tasks; + } + /** * Récupère les checkboxes en attente (non cochées) */ diff --git a/src/components/ui/Calendar.tsx b/src/components/ui/Calendar.tsx index 7e11977..5143ff0 100644 --- a/src/components/ui/Calendar.tsx +++ b/src/components/ui/Calendar.tsx @@ -11,6 +11,7 @@ interface CalendarProps { currentDate: Date; onDateSelect: (date: Date) => void; markedDates?: string[]; // Liste des dates marquées (format YYYY-MM-DD) + deadlineDates?: Record; // Date -> Array de noms de tâches showTodayButton?: boolean; showLegend?: boolean; className?: string; @@ -20,6 +21,7 @@ export function Calendar({ currentDate, onDateSelect, markedDates = [], + deadlineDates = {}, showTodayButton = true, showLegend = true, className = '', @@ -100,6 +102,11 @@ export function Calendar({ return markedDates.includes(formatDateKey(date)); }; + const hasDeadlineDate = (date: Date) => { + const dateKey = formatDateKey(date); + return deadlineDates[dateKey] && deadlineDates[dateKey].length > 0; + }; + const isSelected = (date: Date) => { return formatDateKey(date) === currentDateKey; }; @@ -164,6 +171,7 @@ export function Calendar({ const isCurrentMonthDay = isCurrentMonth(date); const isTodayDay = isTodayDate(date); const hasMarked = hasMarkedDate(date); + const hasDeadline = hasDeadlineDate(date); const isSelectedDay = isSelected(date); return ( @@ -188,15 +196,27 @@ export function Calendar({ > {date.getDate()} - {/* Indicateur de date marquée */} - {hasMarked && ( -
- )} + {/* Indicateurs de dates */} +
+ {/* Indicateur de date marquée (point bleu) */} + {hasMarked && ( +
+ )} + {/* Indicateur de date de fin (point rouge) */} + {hasDeadline && ( +
+ )} +
); })} @@ -213,6 +233,14 @@ export function Calendar({ Jour avec des éléments
+
+
+
+
+ + Date de fin de tâche + +
diff --git a/src/services/task-management/daily.ts b/src/services/task-management/daily.ts index c013f57..3949bef 100644 --- a/src/services/task-management/daily.ts +++ b/src/services/task-management/daily.ts @@ -10,6 +10,7 @@ import { TaskStatus, TaskPriority, TaskSource, + Task, } from '@/lib/types'; import { getPreviousWorkday, @@ -466,6 +467,137 @@ export class DailyService { }); } + /** + * Récupère toutes les dates de fin (dueDate) des tâches non terminées (pour le calendrier) + * Retourne un objet avec les dates comme clés et les tâches associées + */ + async getTaskDeadlineDates( + userId: string + ): Promise> { + const tasks = await prisma.task.findMany({ + where: { + ownerId: userId, + dueDate: { + not: null, + }, + status: { + notIn: ['done', 'cancelled', 'archived'], + }, + }, + select: { + id: true, + title: true, + dueDate: true, + }, + orderBy: { + dueDate: 'asc', + }, + }); + + const datesMap: Record = {}; + + tasks.forEach((task) => { + if (!task.dueDate) return; + // Normaliser la date pour éviter les problèmes de timezone + const normalizedDate = normalizeDate(task.dueDate); + const dateKey = formatDateForAPI(normalizedDate); + + if (!datesMap[dateKey]) { + datesMap[dateKey] = []; + } + datesMap[dateKey].push(task.title); + }); + + return datesMap; + } + + /** + * Récupère les tâches avec deadline pour une date donnée + * Retourne un tableau de tâches complètes + */ + async getTasksByDeadlineDate(userId: string, date: Date): Promise { + const normalizedDate = normalizeDate(date); + const dateKey = formatDateForAPI(normalizedDate); + + const tasks = await prisma.task.findMany({ + where: { + ownerId: userId, + dueDate: { + not: null, + }, + status: { + notIn: ['done', 'cancelled', 'archived'], + }, + }, + include: { + taskTags: { + include: { + tag: true, + }, + }, + primaryTag: true, + _count: { + select: { + dailyCheckboxes: true, + }, + }, + }, + orderBy: { + dueDate: 'asc', + }, + }); + + // Filtrer les tâches dont la date de fin correspond à la date demandée + const filteredTasks = tasks + .filter((task) => { + if (!task.dueDate) return false; + const taskDateKey = formatDateForAPI(normalizeDate(task.dueDate)); + return taskDateKey === dateKey; + }) + .map((task) => ({ + id: task.id, + title: task.title, + description: task.description ?? undefined, + status: task.status as TaskStatus, + priority: task.priority as TaskPriority, + source: task.source as TaskSource, + sourceId: task.sourceId ?? undefined, + tags: task.taskTags.map((tt) => tt.tag.name), + tagDetails: task.taskTags.map((tt) => ({ + id: tt.tag.id, + name: tt.tag.name, + color: tt.tag.color, + isPinned: tt.tag.isPinned, + })), + primaryTagId: task.primaryTagId ?? undefined, + primaryTag: task.primaryTag + ? { + id: task.primaryTag.id, + name: task.primaryTag.name, + color: task.primaryTag.color, + isPinned: task.primaryTag.isPinned, + } + : undefined, + dueDate: task.dueDate ?? undefined, + completedAt: task.completedAt ?? undefined, + createdAt: task.createdAt, + updatedAt: task.updatedAt, + jiraProject: task.jiraProject ?? undefined, + jiraKey: task.jiraKey ?? undefined, + jiraType: task.jiraType ?? undefined, + tfsProject: task.tfsProject ?? undefined, + tfsPullRequestId: task.tfsPullRequestId ?? undefined, + tfsRepository: task.tfsRepository ?? undefined, + tfsSourceBranch: task.tfsSourceBranch ?? undefined, + tfsTargetBranch: task.tfsTargetBranch ?? undefined, + assignee: task.assignee ?? undefined, + ownerId: (task as unknown as { ownerId: string }).ownerId, + todosCount: task._count.dailyCheckboxes, + })); + + return filteredTasks; + } + /** * Récupère toutes les checkboxes non cochées (tâches en attente) */