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.
This commit is contained in:
Julien Froidefond
2025-09-15 18:21:48 +02:00
parent cf2e360ce9
commit 936e0306fc
8 changed files with 381 additions and 46 deletions

View File

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

View File

@@ -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<string[]> {
const response = await httpClient.get<{ dates: string[] }>('/daily/dates');
return response.dates;
}
}
// Instance singleton du client

View File

@@ -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 (
<Card className="p-4">
{/* Header avec navigation */}
<div className="flex items-center justify-between mb-4">
<Button
onClick={goToPreviousMonth}
variant="ghost"
size="sm"
className="text-[var(--foreground)]"
>
</Button>
<h3 className="text-lg font-bold text-[var(--foreground)] capitalize">
{formatMonthYear()}
</h3>
<Button
onClick={goToNextMonth}
variant="ghost"
size="sm"
className="text-[var(--foreground)]"
>
</Button>
</div>
{/* Bouton Aujourd'hui */}
<div className="mb-4 text-center">
<Button
onClick={goToToday}
variant="primary"
size="sm"
>
Aujourd'hui
</Button>
</div>
{/* Jours de la semaine */}
<div className="grid grid-cols-7 gap-1 mb-2">
{weekDays.map((day) => (
<div
key={day}
className="text-center text-xs font-medium text-[var(--muted-foreground)] p-2"
>
{day}
</div>
))}
</div>
{/* Grille du calendrier */}
<div className="grid grid-cols-7 gap-1">
{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 (
<button
key={index}
onClick={() => handleDateClick(date)}
className={`
relative p-2 text-sm rounded transition-all hover:bg-[var(--muted)]/50
${isCurrentMonthDay
? 'text-[var(--foreground)]'
: 'text-[var(--muted-foreground)]'
}
${isTodayDay
? 'bg-[var(--primary)]/20 border border-[var(--primary)]'
: ''
}
${isSelectedDay
? 'bg-[var(--primary)] text-white'
: ''
}
${hasCheckboxes
? 'font-bold'
: ''
}
`}
>
{date.getDate()}
{/* Indicateur de daily existant */}
{hasCheckboxes && (
<div className={`
absolute bottom-1 right-1 w-2 h-2 rounded-full
${isSelectedDay
? 'bg-white'
: 'bg-[var(--primary)]'
}
`} />
)}
</button>
);
})}
</div>
{/* Légende */}
<div className="mt-4 text-xs text-[var(--muted-foreground)] space-y-1">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-[var(--primary)]"></div>
<span>Jour avec des tâches</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border border-[var(--primary)] bg-[var(--primary)]/20"></div>
<span>Aujourd'hui</span>
</div>
</div>
</Card>
);
}

View File

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

View File

@@ -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<string[]> {
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

View File

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

View File

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

View File

@@ -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<string[]>([]);
// 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&apos;hui
</button>
)}
</div>
@@ -350,8 +393,19 @@ export function DailyPageClient() {
{/* Contenu principal */}
<main className="container mx-auto px-4 py-8">
{dailyView && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Calendrier - toujours visible */}
<div className="xl:col-span-1">
<DailyCalendar
currentDate={currentDate}
onDateSelect={handleDateSelect}
dailyDates={dailyDates}
/>
</div>
{/* Sections daily */}
{dailyView && (
<div className="xl:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Section Hier */}
<DailySectionComponent
title="📋 Hier"
@@ -375,8 +429,9 @@ export function DailyPageClient() {
onDeleteCheckbox={handleDeleteCheckbox}
saving={saving}
/>
</div>
)}
</div>
)}
</div>
{/* Footer avec stats */}
{dailyView && (