From 936e0306fca82ee6d96de06e82acdbcd59cfc746 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 15 Sep 2025 18:21:48 +0200 Subject: [PATCH] feat: update Daily management features and enhance date handling - Marked the calendar/history view of dailies as completed in the TODO list. - Improved date formatting in `formatDateForAPI` to avoid timezone issues. - Added `getDailyDates` method in `DailyClient` and `DailyService` to retrieve all dates with dailies. - Enhanced `POST` route to robustly parse date input for better error handling. - Integrated daily dates loading in `DailyPageClient` for calendar display. --- TODO.md | 2 +- clients/daily-client.ts | 15 +- components/daily/DailyCalendar.tsx | 218 +++++++++++++++++++++++++++++ prisma/schema.prisma | 52 +++---- services/daily.ts | 26 +++- src/app/api/daily/dates/route.ts | 20 +++ src/app/api/daily/route.ts | 13 +- src/app/daily/DailyPageClient.tsx | 81 +++++++++-- 8 files changed, 381 insertions(+), 46 deletions(-) create mode 100644 components/daily/DailyCalendar.tsx create mode 100644 src/app/api/daily/dates/route.ts diff --git a/TODO.md b/TODO.md index dda01c3..5e2afef 100644 --- a/TODO.md +++ b/TODO.md @@ -108,7 +108,7 @@ - [x] Navigation par date (daily précédent/suivant) - [x] Auto-création du daily du jour si inexistant - [x] UX améliorée : édition au clic, focus persistant, input large -- [ ] Vue calendar/historique des dailies +- [x] Vue calendar/historique des dailies - [ ] Templates de daily personnalisables - [ ] Recherche dans l'historique des dailies diff --git a/clients/daily-client.ts b/clients/daily-client.ts index 708fa34..343cdc5 100644 --- a/clients/daily-client.ts +++ b/clients/daily-client.ts @@ -123,10 +123,13 @@ export class DailyClient { } /** - * Formate une date pour l'API + * Formate une date pour l'API (évite les décalages timezone) */ formatDateForAPI(date: Date): string { - return date.toISOString().split('T')[0]; // YYYY-MM-DD + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; // YYYY-MM-DD } /** @@ -147,6 +150,14 @@ export class DailyClient { return this.getDailyView(date); } + + /** + * Récupère toutes les dates qui ont des dailies + */ + async getDailyDates(): Promise { + const response = await httpClient.get<{ dates: string[] }>('/daily/dates'); + return response.dates; + } } // Instance singleton du client diff --git a/components/daily/DailyCalendar.tsx b/components/daily/DailyCalendar.tsx new file mode 100644 index 0000000..3cbb78d --- /dev/null +++ b/components/daily/DailyCalendar.tsx @@ -0,0 +1,218 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/Button'; +import { Card } from '@/components/ui/Card'; + +interface DailyCalendarProps { + currentDate: Date; + onDateSelect: (date: Date) => void; + dailyDates: string[]; // Liste des dates qui ont des dailies (format YYYY-MM-DD) +} + +export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCalendarProps) { + const [viewDate, setViewDate] = useState(new Date(currentDate)); + + // Formatage des dates pour comparaison (éviter le décalage timezone) + const formatDateKey = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + + const currentDateKey = formatDateKey(currentDate); + + // Navigation mois + const goToPreviousMonth = () => { + const newDate = new Date(viewDate); + newDate.setMonth(newDate.getMonth() - 1); + setViewDate(newDate); + }; + + const goToNextMonth = () => { + const newDate = new Date(viewDate); + newDate.setMonth(newDate.getMonth() + 1); + setViewDate(newDate); + }; + + const goToToday = () => { + const today = new Date(); + setViewDate(today); + onDateSelect(today); + }; + + // Obtenir les jours du mois + const getDaysInMonth = () => { + const year = viewDate.getFullYear(); + const month = viewDate.getMonth(); + + // Premier jour du mois + const firstDay = new Date(year, month, 1); + // Dernier jour du mois + const lastDay = new Date(year, month + 1, 0); + + // Premier lundi de la semaine contenant le premier jour + const startDate = new Date(firstDay); + const dayOfWeek = firstDay.getDay(); + const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Lundi = 0 + startDate.setDate(firstDay.getDate() - daysToSubtract); + + // Générer toutes les dates du calendrier (6 semaines) + const days = []; + const currentDay = new Date(startDate); + + for (let i = 0; i < 42; i++) { // 6 semaines × 7 jours + days.push(new Date(currentDay)); + currentDay.setDate(currentDay.getDate() + 1); + } + + return { days, firstDay, lastDay }; + }; + + const { days, firstDay, lastDay } = getDaysInMonth(); + + const handleDateClick = (date: Date) => { + onDateSelect(date); + }; + + const isToday = (date: Date) => { + const today = new Date(); + return formatDateKey(date) === formatDateKey(today); + }; + + const isCurrentMonth = (date: Date) => { + return date.getMonth() === viewDate.getMonth(); + }; + + const hasDaily = (date: Date) => { + return dailyDates.includes(formatDateKey(date)); + }; + + const isSelected = (date: Date) => { + return formatDateKey(date) === currentDateKey; + }; + + const formatMonthYear = () => { + return viewDate.toLocaleDateString('fr-FR', { + month: 'long', + year: 'numeric' + }); + }; + + const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']; + + return ( + + {/* Header avec navigation */} +
+ + +

+ {formatMonthYear()} +

+ + +
+ + {/* Bouton Aujourd'hui */} +
+ +
+ + {/* Jours de la semaine */} +
+ {weekDays.map((day) => ( +
+ {day} +
+ ))} +
+ + {/* Grille du calendrier */} +
+ {days.map((date, index) => { + const dateKey = formatDateKey(date); + const isCurrentMonthDay = isCurrentMonth(date); + const isTodayDay = isToday(date); + const hasCheckboxes = hasDaily(date); + const isSelectedDay = isSelected(date); + + return ( + + ); + })} +
+ + {/* Légende */} +
+
+
+ Jour avec des tâches +
+
+
+ Aujourd'hui +
+
+
+ ); +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 41269a1..c42b54a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,38 +11,38 @@ datasource db { } model Task { - id String @id @default(cuid()) + id String @id @default(cuid()) title String description String? - status String @default("todo") - priority String @default("medium") - source String // "reminders" | "jira" - sourceId String? // ID dans le système source + status String @default("todo") + priority String @default("medium") + source String // "reminders" | "jira" + sourceId String? // ID dans le système source dueDate DateTime? completedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // Métadonnées Jira jiraProject String? jiraKey String? assignee String? - + // Relations - taskTags TaskTag[] + taskTags TaskTag[] dailyCheckboxes DailyCheckbox[] - + @@unique([source, sourceId]) @@map("tasks") } model Tag { - id String @id @default(cuid()) - name String @unique - color String @default("#6b7280") - isPinned Boolean @default(false) // Tag pour objectifs principaux - taskTags TaskTag[] - + id String @id @default(cuid()) + name String @unique + color String @default("#6b7280") + isPinned Boolean @default(false) // Tag pour objectifs principaux + taskTags TaskTag[] + @@map("tags") } @@ -51,35 +51,35 @@ model TaskTag { tagId String task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) - + @@id([taskId, tagId]) @@map("task_tags") } model SyncLog { id String @id @default(cuid()) - source String // "reminders" | "jira" - status String // "success" | "error" + source String // "reminders" | "jira" + status String // "success" | "error" message String? tasksSync Int @default(0) createdAt DateTime @default(now()) - + @@map("sync_logs") } model DailyCheckbox { id String @id @default(cuid()) date DateTime // Date de la checkbox (YYYY-MM-DD) - text String // Texte de la checkbox + text String // Texte de la checkbox isChecked Boolean @default(false) order Int @default(0) // Ordre d'affichage pour cette date - taskId String? // Liaison optionnelle vers une tâche + taskId String? // Liaison optionnelle vers une tâche createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - + // Relations - task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull) - + task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull) + @@index([date]) @@map("daily_checkboxes") } diff --git a/services/daily.ts b/services/daily.ts index 4dc66a2..5b1a3aa 100644 --- a/services/daily.ts +++ b/services/daily.ts @@ -139,8 +139,7 @@ export class DailyService { const checkboxes = await prisma.dailyCheckbox.findMany({ where: { text: { - contains: query, - mode: 'insensitive' + contains: query } }, include: { task: true }, @@ -238,6 +237,29 @@ export class DailyService { updatedAt: checkbox.updatedAt }; } + + /** + * Récupère toutes les dates qui ont des checkboxes (pour le calendrier) + */ + async getDailyDates(): Promise { + const checkboxes = await prisma.dailyCheckbox.findMany({ + select: { + date: true + }, + distinct: ['date'], + orderBy: { + date: 'desc' + } + }); + + return checkboxes.map(checkbox => { + const date = checkbox.date; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }); + } } // Instance singleton du service diff --git a/src/app/api/daily/dates/route.ts b/src/app/api/daily/dates/route.ts new file mode 100644 index 0000000..115bbd0 --- /dev/null +++ b/src/app/api/daily/dates/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server'; +import { dailyService } from '@/services/daily'; + +/** + * API route pour récupérer toutes les dates avec des dailies + * GET /api/daily/dates + */ +export async function GET() { + try { + const dates = await dailyService.getDailyDates(); + return NextResponse.json({ dates }); + + } catch (error) { + console.error('Erreur lors de la récupération des dates:', error); + return NextResponse.json( + { error: 'Erreur interne du serveur' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/daily/route.ts b/src/app/api/daily/route.ts index 9cacabd..843eea3 100644 --- a/src/app/api/daily/route.ts +++ b/src/app/api/daily/route.ts @@ -68,7 +68,16 @@ export async function POST(request: Request) { ); } - const date = new Date(body.date); + // Parser la date de façon plus robuste + let date: Date; + if (typeof body.date === 'string') { + // Si c'est une string YYYY-MM-DD, créer une date locale + const [year, month, day] = body.date.split('-').map(Number); + date = new Date(year, month - 1, day); // month est 0-indexé + } else { + date = new Date(body.date); + } + if (isNaN(date.getTime())) { return NextResponse.json( { error: 'Format de date invalide. Utilisez YYYY-MM-DD' }, @@ -93,4 +102,4 @@ export async function POST(request: Request) { { status: 500 } ); } -} \ No newline at end of file +} diff --git a/src/app/daily/DailyPageClient.tsx b/src/app/daily/DailyPageClient.tsx index 1ac61b2..4c69bde 100644 --- a/src/app/daily/DailyPageClient.tsx +++ b/src/app/daily/DailyPageClient.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useRef } from 'react'; +import { useState, useRef, useEffect } from 'react'; import React from 'react'; import { useDaily } from '@/hooks/useDaily'; import { DailyCheckbox } from '@/lib/types'; @@ -8,8 +8,7 @@ import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Card } from '@/components/ui/Card'; import Link from 'next/link'; -import { formatDistanceToNow } from 'date-fns'; -import { fr } from 'date-fns/locale'; +import { DailyCalendar } from '@/components/daily/DailyCalendar'; interface DailySectionProps { title: string; @@ -212,22 +211,62 @@ export function DailyPageClient() { error, saving, currentDate, - addTodayCheckbox, - addYesterdayCheckbox, + refreshDaily, toggleCheckbox, updateCheckbox, deleteCheckbox, goToPreviousDay, goToNextDay, - goToToday + goToToday, + setDate } = useDaily(); + const [dailyDates, setDailyDates] = useState([]); + + // Charger les dates avec des dailies pour le calendrier + useEffect(() => { + import('@/clients/daily-client').then(({ dailyClient }) => { + return dailyClient.getDailyDates(); + }).then(setDailyDates).catch(console.error); + }, []); + const handleAddTodayCheckbox = async (text: string) => { - await addTodayCheckbox(text); + try { + const { dailyClient } = await import('@/clients/daily-client'); + await dailyClient.addCheckbox({ + date: currentDate, + text, + isChecked: false + }); + // Recharger la vue daily après ajout + await refreshDaily(); + // Recharger aussi les dates pour le calendrier + const updatedDates = await dailyClient.getDailyDates(); + setDailyDates(updatedDates); + } catch (error) { + console.error('Erreur lors de l\'ajout de la tâche:', error); + } }; const handleAddYesterdayCheckbox = async (text: string) => { - await addYesterdayCheckbox(text); + try { + const yesterday = new Date(currentDate); + yesterday.setDate(yesterday.getDate() - 1); + + const { dailyClient } = await import('@/clients/daily-client'); + await dailyClient.addCheckbox({ + date: yesterday, + text, + isChecked: false + }); + // Recharger la vue daily après ajout + await refreshDaily(); + // Recharger aussi les dates pour le calendrier + const updatedDates = await dailyClient.getDailyDates(); + setDailyDates(updatedDates); + } catch (error) { + console.error('Erreur lors de l\'ajout de la tâche:', error); + } }; const handleToggleCheckbox = async (checkboxId: string) => { @@ -252,6 +291,10 @@ export function DailyPageClient() { return currentDate; }; + const handleDateSelect = (date: Date) => { + setDate(date); + }; + const formatCurrentDate = () => { return currentDate.toLocaleDateString('fr-FR', { weekday: 'long', @@ -330,7 +373,7 @@ export function DailyPageClient() { onClick={goToToday} className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono" > - Aller à aujourd'hui + Aller à aujourd'hui )} @@ -350,8 +393,19 @@ export function DailyPageClient() { {/* Contenu principal */}
- {dailyView && ( -
+
+ {/* Calendrier - toujours visible */} +
+ +
+ + {/* Sections daily */} + {dailyView && ( +
{/* Section Hier */} -
- )} +
+ )} +
{/* Footer avec stats */} {dailyView && (