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:
63
src/app/api/daily/checkboxes/[id]/route.ts
Normal file
63
src/app/api/daily/checkboxes/[id]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
38
src/app/api/daily/checkboxes/route.ts
Normal file
38
src/app/api/daily/checkboxes/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
96
src/app/api/daily/route.ts
Normal file
96
src/app/api/daily/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
396
src/app/daily/DailyPageClient.tsx
Normal file
396
src/app/daily/DailyPageClient.tsx
Normal 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
11
src/app/daily/page.tsx
Normal 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 />;
|
||||
}
|
||||
Reference in New Issue
Block a user