feat: implement Daily management features and update UI

- Marked tasks as completed in TODO for Daily management service, data model, and interactive checkboxes.
- Added a new link to the Daily page in the Header component for easy navigation.
- Introduced DailyCheckbox model in Prisma schema and corresponding TypeScript interfaces for better data handling.
- Updated database schema to include daily checkboxes, enhancing task management capabilities.
This commit is contained in:
Julien Froidefond
2025-09-15 18:04:46 +02:00
parent 74ef79eb70
commit cf2e360ce9
14 changed files with 1423 additions and 10 deletions

View File

@@ -0,0 +1,63 @@
import { NextResponse } from 'next/server';
import { dailyService } from '@/services/daily';
/**
* API route pour mettre à jour une checkbox
*/
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const body = await request.json();
const { id: checkboxId } = await params;
const checkbox = await dailyService.updateCheckbox(checkboxId, body);
return NextResponse.json(checkbox);
} catch (error) {
console.error('Erreur lors de la mise à jour de la checkbox:', error);
if (error instanceof Error && error.message.includes('Record to update not found')) {
return NextResponse.json(
{ error: 'Checkbox non trouvée' },
{ status: 404 }
);
}
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}
/**
* API route pour supprimer une checkbox
*/
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: checkboxId } = await params;
await dailyService.deleteCheckbox(checkboxId);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Erreur lors de la suppression de la checkbox:', error);
if (error instanceof Error && error.message.includes('Checkbox non trouvée')) {
return NextResponse.json(
{ error: 'Checkbox non trouvée' },
{ status: 404 }
);
}
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,38 @@
import { NextResponse } from 'next/server';
import { dailyService } from '@/services/daily';
/**
* API route pour réordonner les checkboxes d'une date
*/
export async function POST(request: Request) {
try {
const body = await request.json();
// Validation des données
if (!body.date || !Array.isArray(body.checkboxIds)) {
return NextResponse.json(
{ error: 'date et checkboxIds (array) sont requis' },
{ status: 400 }
);
}
const date = new Date(body.date);
if (isNaN(date.getTime())) {
return NextResponse.json(
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
{ status: 400 }
);
}
await dailyService.reorderCheckboxes(date, body.checkboxIds);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Erreur lors du réordonnancement:', error);
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,96 @@
import { NextResponse } from 'next/server';
import { dailyService } from '@/services/daily';
/**
* API route pour récupérer la vue daily (hier + aujourd'hui)
*/
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const action = searchParams.get('action');
const date = searchParams.get('date');
if (action === 'history') {
// Récupérer l'historique
const limit = parseInt(searchParams.get('limit') || '30');
const history = await dailyService.getCheckboxHistory(limit);
return NextResponse.json(history);
}
if (action === 'search') {
// Recherche dans les checkboxes
const query = searchParams.get('q') || '';
const limit = parseInt(searchParams.get('limit') || '20');
if (!query.trim()) {
return NextResponse.json({ error: 'Query parameter required' }, { status: 400 });
}
const checkboxes = await dailyService.searchCheckboxes(query, limit);
return NextResponse.json(checkboxes);
}
// Vue daily pour une date donnée (ou aujourd'hui par défaut)
const targetDate = date ? new Date(date) : new Date();
if (date && isNaN(targetDate.getTime())) {
return NextResponse.json(
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
{ status: 400 }
);
}
const dailyView = await dailyService.getDailyView(targetDate);
return NextResponse.json(dailyView);
} catch (error) {
console.error('Erreur lors de la récupération du daily:', error);
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}
/**
* API route pour ajouter une checkbox
*/
export async function POST(request: Request) {
try {
const body = await request.json();
// Validation des données
if (!body.date || !body.text) {
return NextResponse.json(
{ error: 'Date et text sont requis' },
{ status: 400 }
);
}
const date = new Date(body.date);
if (isNaN(date.getTime())) {
return NextResponse.json(
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
{ status: 400 }
);
}
const checkbox = await dailyService.addCheckbox({
date,
text: body.text,
taskId: body.taskId,
order: body.order,
isChecked: body.isChecked
});
return NextResponse.json(checkbox, { status: 201 });
} catch (error) {
console.error('Erreur lors de l\'ajout de la checkbox:', error);
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,396 @@
'use client';
import { useState, useRef } from 'react';
import React from 'react';
import { useDaily } from '@/hooks/useDaily';
import { DailyCheckbox } from '@/lib/types';
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';
interface DailySectionProps {
title: string;
date: Date;
checkboxes: DailyCheckbox[];
onAddCheckbox: (text: string) => Promise<void>;
onToggleCheckbox: (checkboxId: string) => Promise<void>;
onUpdateCheckbox: (checkboxId: string, text: string) => Promise<void>;
onDeleteCheckbox: (checkboxId: string) => Promise<void>;
saving: boolean;
}
function DailySectionComponent({
title,
date,
checkboxes,
onAddCheckbox,
onToggleCheckbox,
onUpdateCheckbox,
onDeleteCheckbox,
saving
}: DailySectionProps) {
const [newCheckboxText, setNewCheckboxText] = useState('');
const [addingCheckbox, setAddingCheckbox] = useState(false);
const [editingCheckboxId, setEditingCheckboxId] = useState<string | null>(null);
const [editingText, setEditingText] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const formatShortDate = (date: Date) => {
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const handleAddCheckbox = async () => {
if (!newCheckboxText.trim()) return;
setAddingCheckbox(true);
try {
await onAddCheckbox(newCheckboxText.trim());
setNewCheckboxText('');
// Garder le focus sur l'input pour enchainer les entrées
setTimeout(() => {
inputRef.current?.focus();
}, 100);
} finally {
setAddingCheckbox(false);
}
};
const handleStartEdit = (checkbox: DailyCheckbox) => {
setEditingCheckboxId(checkbox.id);
setEditingText(checkbox.text);
};
const handleSaveEdit = async () => {
if (!editingCheckboxId || !editingText.trim()) return;
try {
await onUpdateCheckbox(editingCheckboxId, editingText.trim());
setEditingCheckboxId(null);
setEditingText('');
} catch (error) {
console.error('Erreur lors de la modification:', error);
}
};
const handleCancelEdit = () => {
setEditingCheckboxId(null);
setEditingText('');
};
const handleEditKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSaveEdit();
} else if (e.key === 'Escape') {
e.preventDefault();
handleCancelEdit();
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCheckbox();
}
};
return (
<Card className="p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-[var(--foreground)] font-mono">
{title} <span className="text-sm font-normal text-[var(--muted-foreground)]">({formatShortDate(date)})</span>
</h2>
<span className="text-xs text-[var(--muted-foreground)] font-mono">
{checkboxes.filter(cb => cb.isChecked).length}/{checkboxes.length}
</span>
</div>
{/* Liste des checkboxes */}
<div className="space-y-2 mb-4">
{checkboxes.map((checkbox) => (
<div
key={checkbox.id}
className="flex items-center gap-3 p-2 rounded border border-[var(--border)]/30 hover:border-[var(--border)] transition-colors group"
>
<input
type="checkbox"
checked={checkbox.isChecked}
onChange={() => onToggleCheckbox(checkbox.id)}
disabled={saving}
className="w-4 h-4 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-2"
/>
{editingCheckboxId === checkbox.id ? (
<Input
value={editingText}
onChange={(e) => setEditingText(e.target.value)}
onKeyDown={handleEditKeyPress}
onBlur={handleSaveEdit}
autoFocus
className="flex-1 h-8 text-sm"
/>
) : (
<span
className={`flex-1 text-sm font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 p-1 rounded ${
checkbox.isChecked
? 'line-through text-[var(--muted-foreground)]'
: 'text-[var(--foreground)]'
}`}
onClick={() => handleStartEdit(checkbox)}
>
{checkbox.text}
</span>
)}
{/* Lien vers la tâche si liée */}
{checkbox.task && (
<Link
href={`/?highlight=${checkbox.task.id}`}
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono"
title={`Tâche: ${checkbox.task.title}`}
>
#{checkbox.task.id.slice(-6)}
</Link>
)}
{/* Bouton de suppression */}
<button
onClick={() => onDeleteCheckbox(checkbox.id)}
disabled={saving}
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-full bg-[var(--destructive)]/20 hover:bg-[var(--destructive)]/30 border border-[var(--destructive)]/30 hover:border-[var(--destructive)]/50 flex items-center justify-center transition-all duration-200 text-[var(--destructive)] text-xs"
title="Supprimer"
>
×
</button>
</div>
))}
{checkboxes.length === 0 && (
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm font-mono">
Aucune tâche pour cette période
</div>
)}
</div>
{/* Formulaire d'ajout */}
<div className="flex gap-2">
<Input
ref={inputRef}
type="text"
placeholder={`Ajouter une tâche...`}
value={newCheckboxText}
onChange={(e) => setNewCheckboxText(e.target.value)}
onKeyDown={handleKeyPress}
disabled={addingCheckbox || saving}
className="flex-1 min-w-[300px]"
/>
<Button
onClick={handleAddCheckbox}
disabled={!newCheckboxText.trim() || addingCheckbox || saving}
variant="primary"
size="sm"
className="min-w-[40px]"
>
{addingCheckbox ? '...' : '+'}
</Button>
</div>
</Card>
);
}
export function DailyPageClient() {
const {
dailyView,
loading,
error,
saving,
currentDate,
addTodayCheckbox,
addYesterdayCheckbox,
toggleCheckbox,
updateCheckbox,
deleteCheckbox,
goToPreviousDay,
goToNextDay,
goToToday
} = useDaily();
const handleAddTodayCheckbox = async (text: string) => {
await addTodayCheckbox(text);
};
const handleAddYesterdayCheckbox = async (text: string) => {
await addYesterdayCheckbox(text);
};
const handleToggleCheckbox = async (checkboxId: string) => {
await toggleCheckbox(checkboxId);
};
const handleDeleteCheckbox = async (checkboxId: string) => {
await deleteCheckbox(checkboxId);
};
const handleUpdateCheckbox = async (checkboxId: string, text: string) => {
await updateCheckbox(checkboxId, { text });
};
const getYesterdayDate = () => {
const yesterday = new Date(currentDate);
yesterday.setDate(yesterday.getDate() - 1);
return yesterday;
};
const getTodayDate = () => {
return currentDate;
};
const formatCurrentDate = () => {
return currentDate.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const isToday = () => {
const today = new Date();
return currentDate.toDateString() === today.toDateString();
};
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-center min-h-[200px]">
<div className="text-[var(--muted-foreground)] font-mono">
Chargement du daily...
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="container mx-auto px-4 py-8">
<div className="bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded-lg p-4 text-center">
<p className="text-[var(--destructive)] font-mono mb-4">
Erreur: {error}
</p>
<Button onClick={() => window.location.reload()} variant="primary">
Réessayer
</Button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[var(--background)]">
{/* Header */}
<header className="bg-[var(--card)]/80 backdrop-blur-sm border-b border-[var(--border)]/50 sticky top-0 z-10">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link
href="/"
className="text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono text-sm"
>
Kanban
</Link>
<h1 className="text-xl font-bold text-[var(--foreground)] font-mono">
📝 Daily
</h1>
</div>
<div className="flex items-center gap-2">
<Button
onClick={goToPreviousDay}
variant="ghost"
size="sm"
disabled={saving}
>
</Button>
<div className="text-center min-w-[200px]">
<div className="text-sm font-bold text-[var(--foreground)] font-mono">
{formatCurrentDate()}
</div>
{!isToday() && (
<button
onClick={goToToday}
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono"
>
Aller à aujourd'hui
</button>
)}
</div>
<Button
onClick={goToNextDay}
variant="ghost"
size="sm"
disabled={saving}
>
</Button>
</div>
</div>
</div>
</header>
{/* Contenu principal */}
<main className="container mx-auto px-4 py-8">
{dailyView && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Section Hier */}
<DailySectionComponent
title="📋 Hier"
date={getYesterdayDate()}
checkboxes={dailyView.yesterday}
onAddCheckbox={handleAddYesterdayCheckbox}
onToggleCheckbox={handleToggleCheckbox}
onUpdateCheckbox={handleUpdateCheckbox}
onDeleteCheckbox={handleDeleteCheckbox}
saving={saving}
/>
{/* Section Aujourd'hui */}
<DailySectionComponent
title="🎯 Aujourd'hui"
date={getTodayDate()}
checkboxes={dailyView.today}
onAddCheckbox={handleAddTodayCheckbox}
onToggleCheckbox={handleToggleCheckbox}
onUpdateCheckbox={handleUpdateCheckbox}
onDeleteCheckbox={handleDeleteCheckbox}
saving={saving}
/>
</div>
)}
{/* Footer avec stats */}
{dailyView && (
<Card className="mt-8 p-4">
<div className="text-center text-sm text-[var(--muted-foreground)] font-mono">
Daily pour {formatCurrentDate()}
{' • '}
{dailyView.yesterday.length + dailyView.today.length} tâche{dailyView.yesterday.length + dailyView.today.length > 1 ? 's' : ''} au total
{' • '}
{dailyView.yesterday.filter(cb => cb.isChecked).length + dailyView.today.filter(cb => cb.isChecked).length} complétée{(dailyView.yesterday.filter(cb => cb.isChecked).length + dailyView.today.filter(cb => cb.isChecked).length) > 1 ? 's' : ''}
</div>
</Card>
)}
</main>
</div>
);
}

11
src/app/daily/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Metadata } from 'next';
import { DailyPageClient } from './DailyPageClient';
export const metadata: Metadata = {
title: 'Daily - Tower Control',
description: 'Gestion quotidienne des tâches et objectifs',
};
export default function DailyPage() {
return <DailyPageClient />;
}