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:
2
TODO.md
2
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
218
components/daily/DailyCalendar.tsx
Normal file
218
components/daily/DailyCalendar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -11,17 +11,17 @@ 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?
|
||||
@@ -29,7 +29,7 @@ model Task {
|
||||
assignee String?
|
||||
|
||||
// Relations
|
||||
taskTags TaskTag[]
|
||||
taskTags TaskTag[]
|
||||
dailyCheckboxes DailyCheckbox[]
|
||||
|
||||
@@unique([source, sourceId])
|
||||
@@ -37,11 +37,11 @@ model Task {
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -58,8 +58,8 @@ model TaskTag {
|
||||
|
||||
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())
|
||||
@@ -70,15 +70,15 @@ model SyncLog {
|
||||
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")
|
||||
|
||||
@@ -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
|
||||
|
||||
20
src/app/api/daily/dates/route.ts
Normal file
20
src/app/api/daily/dates/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
|
||||
@@ -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'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 && (
|
||||
|
||||
Reference in New Issue
Block a user