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.
This commit is contained in:
Julien Froidefond
2025-11-11 08:46:19 +01:00
parent f7c9926348
commit 8340008839
8 changed files with 435 additions and 29 deletions

View File

@@ -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);

View File

@@ -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 }
);
}
}

View File

@@ -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<string, string[]> } 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 }
);
}
}

View File

@@ -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<string, string[]>; // 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<string[]>(initialDailyDates);
const [deadlineDates, setDeadlineDates] =
useState<Record<string, string[]>>(initialDeadlineDates);
const [deadlineTasks, setDeadlineTasks] = useState<Task[]>([]);
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 && (
<Card variant="glass">
<CardHeader padding="sm" separator={false}>
<CardTitle size="sm" className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-[var(--destructive)]"></div>
Tâches à terminer
</CardTitle>
</CardHeader>
<CardContent padding="sm">
{loadingDeadlineTasks ? (
<div className="text-sm text-[var(--muted-foreground)] text-center py-4">
Chargement...
</div>
) : (
<div className="space-y-2">
{deadlineTasks.map((task) => (
<TaskCard
key={task.id}
variant="compact"
title={task.title}
description={task.description}
tags={task.tags}
primaryTagId={task.primaryTagId}
priority={task.priority}
status={task.status}
dueDate={task.dueDate}
source={task.source}
jiraKey={task.jiraKey}
jiraProject={task.jiraProject}
jiraType={task.jiraType}
todosCount={task.todosCount}
availableTags={availableTags}
fontSize="small"
onClick={() => {
window.location.href = `/kanban?taskId=${task.id}`;
}}
/>
))}
</div>
)}
</CardContent>
</Card>
)}
</div>
)}
</div>
@@ -362,14 +453,60 @@ export function DailyPageClient({
<div className="hidden sm:block">
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Calendrier - Desktop */}
<div className="xl:col-span-1">
<div className="xl:col-span-1 space-y-6">
<Calendar
currentDate={currentDate}
onDateSelect={handleDateSelect}
markedDates={dailyDates}
deadlineDates={deadlineDates}
showTodayButton={true}
showLegend={true}
/>
{/* Section des tâches avec deadline pour la date sélectionnée */}
{deadlineTasks.length > 0 && (
<Card variant="glass">
<CardHeader padding="sm" separator={false}>
<CardTitle size="sm" className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-[var(--destructive)]"></div>
Tâches à terminer
</CardTitle>
</CardHeader>
<CardContent padding="sm">
{loadingDeadlineTasks ? (
<div className="text-sm text-[var(--muted-foreground)] text-center py-4">
Chargement...
</div>
) : (
<div className="space-y-2">
{deadlineTasks.map((task) => (
<TaskCard
key={task.id}
variant="compact"
title={task.title}
description={task.description}
tags={task.tags}
primaryTagId={task.primaryTagId}
priority={task.priority}
status={task.status}
dueDate={task.dueDate}
source={task.source}
jiraKey={task.jiraKey}
jiraProject={task.jiraProject}
jiraType={task.jiraType}
todosCount={task.todosCount}
availableTags={availableTags}
fontSize="small"
onClick={() => {
window.location.href = `/kanban?taskId=${task.id}`;
}}
/>
))}
</div>
)}
</CardContent>
</Card>
)}
</div>
{/* Sections daily - Desktop */}

View File

@@ -37,10 +37,16 @@ export default async function DailyPage() {
const today = getToday();
try {
const [dailyView, dailyDates, deadlineMetrics, pendingTasks] =
await Promise.all([
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
@@ -58,6 +64,7 @@ export default async function DailyPage() {
<DailyPageClient
initialDailyView={dailyView}
initialDailyDates={dailyDates}
initialDeadlineDates={deadlineDatesMap}
initialDate={today}
initialDeadlineMetrics={deadlineMetrics}
initialPendingTasks={pendingTasks}

View File

@@ -174,6 +174,29 @@ export class DailyClient {
return response.dates;
}
/**
* Récupère toutes les dates de fin des tâches avec leurs noms
* Retourne un objet Record<string, string[]> où chaque clé est une date (YYYY-MM-DD)
* et la valeur est un tableau de noms de tâches
*/
async getDeadlineDates(): Promise<Record<string, string[]>> {
const response = await httpClient.get<{ dates: Record<string, string[]> }>(
'/daily/deadlines'
);
return response.dates;
}
/**
* Récupère les tâches avec deadline pour une date donnée
*/
async getDeadlineTasksForDate(date: Date): Promise<Task[]> {
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)
*/

View File

@@ -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<string, string[]>; // 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 */}
{/* Indicateurs de dates */}
<div className="absolute bottom-1 right-1 flex gap-0.5">
{/* Indicateur de date marquée (point bleu) */}
{hasMarked && (
<div
className={`
absolute bottom-1 right-1 w-2 h-2 rounded-full
w-2 h-2 rounded-full
${isSelectedDay ? 'bg-white' : 'bg-[var(--primary)]'}
`}
/>
)}
{/* Indicateur de date de fin (point rouge) */}
{hasDeadline && (
<div
className={`
w-2 h-2 rounded-full
${isSelectedDay ? 'bg-white' : 'bg-[var(--destructive)]'}
`}
/>
)}
</div>
</button>
);
})}
@@ -213,6 +233,14 @@ export function Calendar({
Jour avec des éléments
</span>
</div>
<div className="flex items-center gap-3">
<div className="w-8 flex justify-center">
<div className="w-2 h-2 rounded-full bg-[var(--destructive)]"></div>
</div>
<span className="text-xs text-left flex-1">
Date de fin de tâche
</span>
</div>
<div className="flex items-center gap-3">
<div className="w-8 flex justify-center">
<div className="w-3 h-3 rounded border border-[var(--primary)] bg-[var(--primary)]/20"></div>

View File

@@ -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<Record<string, string[]>> {
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<string, string[]> = {};
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<Task[]> {
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)
*/