From 0b7e0edb2f099670da7c0fa4effa9ba7c830b7f2 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 14 Sep 2025 08:48:39 +0200 Subject: [PATCH] feat: enhance Kanban functionality and update TODO.md - Completed the creation and validation forms for tasks in the Kanban board, improving task management capabilities. - Integrated new task creation and deletion functionalities in the `KanbanBoard` and `KanbanColumn` components. - Added quick task addition feature in `Column` component for better user experience. - Updated `TaskCard` to support task deletion with a new button. - Marked several tasks as completed in `TODO.md` to reflect the progress on Kanban features. - Updated TypeScript types to include 'manual' as a new task source. --- TODO.md | 36 ++-- clients/base/http-client.ts | 72 ++++++++ clients/tasks-client.ts | 114 +++++++++++++ components/forms/CreateTaskForm.tsx | 247 +++++++++++++++++++++++++++ components/kanban/Board.tsx | 49 +++++- components/kanban/BoardContainer.tsx | 32 ++++ components/kanban/Column.tsx | 41 ++++- components/kanban/QuickAddTask.tsx | 225 ++++++++++++++++++++++++ components/kanban/TaskCard.tsx | 41 ++++- components/ui/HeaderContainer.tsx | 57 +++++++ hooks/useTasks.ts | 154 +++++++++++++++++ lib/types.ts | 2 +- src/app/page.tsx | 19 ++- tsconfig.json | 4 +- 14 files changed, 1056 insertions(+), 37 deletions(-) create mode 100644 clients/base/http-client.ts create mode 100644 clients/tasks-client.ts create mode 100644 components/forms/CreateTaskForm.tsx create mode 100644 components/kanban/BoardContainer.tsx create mode 100644 components/kanban/QuickAddTask.tsx create mode 100644 components/ui/HeaderContainer.tsx create mode 100644 hooks/useTasks.ts diff --git a/TODO.md b/TODO.md index 6618240..03bbb46 100644 --- a/TODO.md +++ b/TODO.md @@ -38,11 +38,12 @@ - [x] Refactoriser les composants pour utiliser le nouveau système UI ### 2.3 Gestion des tâches (CRUD) -- [ ] Formulaire de création de tâche (Modal + Form) +- [x] Formulaire de création de tâche (Modal + Form) +- [x] Création rapide inline dans les colonnes (QuickAddTask) - [ ] Formulaire d'édition de tâche (Modal + Form avec pré-remplissage) -- [ ] Suppression de tâche (confirmation + API call) +- [x] Suppression de tâche (icône discrète + API call) - [ ] Changement de statut par drag & drop ou boutons -- [ ] Validation des formulaires et gestion d'erreurs +- [x] Validation des formulaires et gestion d'erreurs ### 2.4 Gestion des tags - [ ] Créer/éditer des tags avec sélecteur de couleur @@ -52,13 +53,14 @@ - [ ] Filtrage par tags ### 2.5 Clients HTTP et hooks -- [ ] `clients/tasks-client.ts` - Client pour les tâches (CRUD) +- [x] `clients/tasks-client.ts` - Client pour les tâches (CRUD complet) - [ ] `clients/tags-client.ts` - Client pour les tags -- [ ] `clients/base/http-client.ts` - Client HTTP de base -- [ ] `hooks/useTasks.ts` - Hook pour la gestion des tâches +- [x] `clients/base/http-client.ts` - Client HTTP de base +- [x] `hooks/useTasks.ts` - Hook pour la gestion des tâches (CRUD complet) - [ ] `hooks/useTags.ts` - Hook pour la gestion des tags - [ ] `hooks/useKanban.ts` - Hook pour drag & drop -- [ ] Gestion des erreurs et loading states +- [x] Gestion des erreurs et loading states +- [x] Architecture SSR + hydratation client optimisée ### 2.6 Fonctionnalités Kanban avancées - [ ] Drag & drop entre colonnes (react-beautiful-dnd) @@ -143,11 +145,21 @@ lib/ ## 🎯 Prochaines étapes immédiates -1. **Créer les composants UI de base** (Button, Input, Card, Modal) -2. **Implémenter le système de design** avec Tailwind -3. **Améliorer le Kanban** avec un design moderne -4. **Ajouter drag & drop** entre les colonnes -5. **Créer les formulaires** de tâches +1. **Formulaire d'édition de tâche** - Modal avec pré-remplissage des données +2. **Drag & drop entre colonnes** - react-beautiful-dnd pour changer les statuts +3. **Gestion avancée des tags** - Couleurs, autocomplete, filtrage +4. **Recherche et filtres** - Filtrage temps réel par titre, tags, statut +5. **Dashboard et analytics** - Graphiques de productivité + +## ✅ **Fonctionnalités terminées (Phase 2.1-2.3)** + +- ✅ Système de design tech dark complet +- ✅ Composants UI de base (Button, Input, Card, Modal, Badge) +- ✅ Architecture SSR + hydratation client +- ✅ CRUD tâches complet (création, suppression) +- ✅ Création rapide inline (QuickAddTask) +- ✅ Client HTTP et hooks React +- ✅ Refactoring Kanban avec nouveaux composants --- diff --git a/clients/base/http-client.ts b/clients/base/http-client.ts new file mode 100644 index 0000000..18f0d50 --- /dev/null +++ b/clients/base/http-client.ts @@ -0,0 +1,72 @@ +/** + * Client HTTP de base pour toutes les requêtes API + */ +export class HttpClient { + private baseUrl: string; + + constructor(baseUrl: string = '') { + this.baseUrl = baseUrl; + } + + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + + const config: RequestInit = { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }; + + try { + const response = await fetch(url, config); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + console.error(`HTTP Request failed: ${url}`, error); + throw error; + } + } + + async get(endpoint: string, params?: Record): Promise { + const url = params + ? `${endpoint}?${new URLSearchParams(params)}` + : endpoint; + + return this.request(url, { method: 'GET' }); + } + + async post(endpoint: string, data?: unknown): Promise { + return this.request(endpoint, { + method: 'POST', + body: data ? JSON.stringify(data) : undefined, + }); + } + + async patch(endpoint: string, data?: unknown): Promise { + return this.request(endpoint, { + method: 'PATCH', + body: data ? JSON.stringify(data) : undefined, + }); + } + + async delete(endpoint: string, params?: Record): Promise { + const url = params + ? `${endpoint}?${new URLSearchParams(params)}` + : endpoint; + + return this.request(url, { method: 'DELETE' }); + } +} + +// Instance par défaut +export const httpClient = new HttpClient('/api'); diff --git a/clients/tasks-client.ts b/clients/tasks-client.ts new file mode 100644 index 0000000..5697461 --- /dev/null +++ b/clients/tasks-client.ts @@ -0,0 +1,114 @@ +import { httpClient } from './base/http-client'; +import { Task, TaskStatus, TaskPriority } from '@/lib/types'; + +export interface TaskFilters { + status?: TaskStatus[]; + source?: string[]; + search?: string; + limit?: number; + offset?: number; +} + +export interface TasksResponse { + success: boolean; + data: Task[]; + stats: { + total: number; + completed: number; + inProgress: number; + todo: number; + completionRate: number; + }; + count: number; +} + +export interface CreateTaskData { + title: string; + description?: string; + status?: TaskStatus; + priority?: TaskPriority; + tags?: string[]; + dueDate?: Date; +} + +export interface UpdateTaskData { + taskId: string; + title?: string; + description?: string; + status?: TaskStatus; + priority?: TaskPriority; + tags?: string[]; + dueDate?: Date; +} + +/** + * Client pour la gestion des tâches + */ +export class TasksClient { + + /** + * Récupère toutes les tâches avec filtres + */ + async getTasks(filters?: TaskFilters): Promise { + const params: Record = {}; + + if (filters?.status) { + params.status = filters.status.join(','); + } + if (filters?.source) { + params.source = filters.source.join(','); + } + if (filters?.search) { + params.search = filters.search; + } + if (filters?.limit) { + params.limit = filters.limit.toString(); + } + if (filters?.offset) { + params.offset = filters.offset.toString(); + } + + return httpClient.get('/tasks', params); + } + + /** + * Crée une nouvelle tâche + */ + async createTask(data: CreateTaskData): Promise<{ success: boolean; data: Task; message: string }> { + const payload = { + ...data, + dueDate: data.dueDate?.toISOString() + }; + + return httpClient.post('/tasks', payload); + } + + /** + * Met à jour une tâche + */ + async updateTask(data: UpdateTaskData): Promise<{ success: boolean; data: Task; message: string }> { + const payload = { + ...data, + dueDate: data.dueDate?.toISOString() + }; + + return httpClient.patch('/tasks', payload); + } + + /** + * Supprime une tâche + */ + async deleteTask(taskId: string): Promise<{ success: boolean; message: string }> { + return httpClient.delete('/tasks', { taskId }); + } + + /** + * Met à jour le statut d'une tâche + */ + async updateTaskStatus(taskId: string, status: TaskStatus): Promise<{ success: boolean; data: Task; message: string }> { + return this.updateTask({ taskId, status }); + } +} + +// Instance singleton +export const tasksClient = new TasksClient(); diff --git a/components/forms/CreateTaskForm.tsx b/components/forms/CreateTaskForm.tsx new file mode 100644 index 0000000..4617af2 --- /dev/null +++ b/components/forms/CreateTaskForm.tsx @@ -0,0 +1,247 @@ +'use client'; + +import { useState } from 'react'; +import { Modal } from '@/components/ui/Modal'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { Badge } from '@/components/ui/Badge'; +import { TaskPriority, TaskStatus } from '@/lib/types'; +import { CreateTaskData } from '@/clients/tasks-client'; + +interface CreateTaskFormProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (data: CreateTaskData) => Promise; + loading?: boolean; +} + +export function CreateTaskForm({ isOpen, onClose, onSubmit, loading = false }: CreateTaskFormProps) { + const [formData, setFormData] = useState({ + title: '', + description: '', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + tags: [], + dueDate: undefined + }); + + const [tagInput, setTagInput] = useState(''); + const [errors, setErrors] = useState>({}); + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + if (!formData.title.trim()) { + newErrors.title = 'Le titre est requis'; + } + + if (formData.title.length > 200) { + newErrors.title = 'Le titre ne peut pas dépasser 200 caractères'; + } + + if (formData.description && formData.description.length > 1000) { + newErrors.description = 'La description ne peut pas dépasser 1000 caractères'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + try { + await onSubmit(formData); + handleClose(); + } catch (error) { + console.error('Erreur lors de la création:', error); + } + }; + + const handleClose = () => { + setFormData({ + title: '', + description: '', + status: 'todo', + priority: 'medium', + tags: [], + dueDate: undefined + }); + setTagInput(''); + setErrors({}); + onClose(); + }; + + const addTag = () => { + const tag = tagInput.trim(); + if (tag && !formData.tags?.includes(tag)) { + setFormData((prev: CreateTaskData) => ({ + ...prev, + tags: [...(prev.tags || []), tag] + })); + setTagInput(''); + } + }; + + const removeTag = (tagToRemove: string) => { + setFormData((prev: CreateTaskData) => ({ + ...prev, + tags: prev.tags?.filter((tag: string) => tag !== tagToRemove) || [] + })); + }; + + const handleTagKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + addTag(); + } + }; + + return ( + +
+ {/* Titre */} + setFormData((prev: CreateTaskData) => ({ ...prev, title: e.target.value }))} + placeholder="Titre de la tâche..." + error={errors.title} + disabled={loading} + /> + + {/* Description */} +
+ +