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.
This commit is contained in:
36
TODO.md
36
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
|
||||
|
||||
---
|
||||
|
||||
|
||||
72
clients/base/http-client.ts
Normal file
72
clients/base/http-client.ts
Normal file
@@ -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<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
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<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
|
||||
const url = params
|
||||
? `${endpoint}?${new URLSearchParams(params)}`
|
||||
: endpoint;
|
||||
|
||||
return this.request<T>(url, { method: 'GET' });
|
||||
}
|
||||
|
||||
async post<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async delete<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
|
||||
const url = params
|
||||
? `${endpoint}?${new URLSearchParams(params)}`
|
||||
: endpoint;
|
||||
|
||||
return this.request<T>(url, { method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
// Instance par défaut
|
||||
export const httpClient = new HttpClient('/api');
|
||||
114
clients/tasks-client.ts
Normal file
114
clients/tasks-client.ts
Normal file
@@ -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<TasksResponse> {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
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<TasksResponse>('/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();
|
||||
247
components/forms/CreateTaskForm.tsx
Normal file
247
components/forms/CreateTaskForm.tsx
Normal file
@@ -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<void>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function CreateTaskForm({ isOpen, onClose, onSubmit, loading = false }: CreateTaskFormProps) {
|
||||
const [formData, setFormData] = useState<CreateTaskData>({
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'todo' as TaskStatus,
|
||||
priority: 'medium' as TaskPriority,
|
||||
tags: [],
|
||||
dueDate: undefined
|
||||
});
|
||||
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
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 (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} title="Nouvelle tâche" size="lg">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Titre */}
|
||||
<Input
|
||||
label="Titre *"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData((prev: CreateTaskData) => ({ ...prev, title: e.target.value }))}
|
||||
placeholder="Titre de la tâche..."
|
||||
error={errors.title}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData((prev: CreateTaskData) => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Description détaillée..."
|
||||
rows={4}
|
||||
disabled={loading}
|
||||
className="w-full px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg text-slate-100 font-mono text-sm placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 hover:border-slate-600/50 transition-all duration-200 backdrop-blur-sm resize-none"
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="text-xs font-mono text-red-400 flex items-center gap-1">
|
||||
<span className="text-red-500">⚠</span>
|
||||
{errors.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priorité et Statut */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
|
||||
Priorité
|
||||
</label>
|
||||
<select
|
||||
value={formData.priority}
|
||||
onChange={(e) => setFormData((prev: CreateTaskData) => ({ ...prev, priority: e.target.value as TaskPriority }))}
|
||||
disabled={loading}
|
||||
className="w-full px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg text-slate-100 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 hover:border-slate-600/50 transition-all duration-200 backdrop-blur-sm"
|
||||
>
|
||||
<option value="low">Faible</option>
|
||||
<option value="medium">Moyenne</option>
|
||||
<option value="high">Élevée</option>
|
||||
<option value="urgent">Urgente</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
|
||||
Statut
|
||||
</label>
|
||||
<select
|
||||
value={formData.status}
|
||||
onChange={(e) => setFormData((prev: CreateTaskData) => ({ ...prev, status: e.target.value as TaskStatus }))}
|
||||
disabled={loading}
|
||||
className="w-full px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg text-slate-100 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 hover:border-slate-600/50 transition-all duration-200 backdrop-blur-sm"
|
||||
>
|
||||
<option value="todo">À faire</option>
|
||||
<option value="in_progress">En cours</option>
|
||||
<option value="done">Terminé</option>
|
||||
<option value="cancelled">Annulé</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date d'échéance */}
|
||||
<Input
|
||||
label="Date d'échéance"
|
||||
type="datetime-local"
|
||||
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''}
|
||||
onChange={(e) => setFormData((prev: CreateTaskData) => ({
|
||||
...prev,
|
||||
dueDate: e.target.value ? new Date(e.target.value) : undefined
|
||||
}))}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
|
||||
Tags
|
||||
</label>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyPress={handleTagKeyPress}
|
||||
placeholder="Ajouter un tag..."
|
||||
disabled={loading}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={addTag}
|
||||
disabled={!tagInput.trim() || loading}
|
||||
>
|
||||
Ajouter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{formData.tags && formData.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.tags.map((tag: string, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="primary"
|
||||
className="cursor-pointer hover:bg-red-950/50 hover:text-red-300 hover:border-red-500/30 transition-colors"
|
||||
onClick={() => removeTag(tag)}
|
||||
>
|
||||
{tag} ✕
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-slate-700/50">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={handleClose}
|
||||
disabled={loading}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Création...' : 'Créer la tâche'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
import { Task, TaskStatus } from '@/lib/types';
|
||||
import { KanbanColumn } from './Column';
|
||||
import { useMemo } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
|
||||
import { CreateTaskData } from '@/clients/tasks-client';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
interface KanbanBoardProps {
|
||||
tasks: Task[];
|
||||
onCreateTask?: (data: CreateTaskData) => Promise<Task | null>;
|
||||
onDeleteTask?: (taskId: string) => Promise<void>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function KanbanBoard({ tasks }: KanbanBoardProps) {
|
||||
export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, loading = false }: KanbanBoardProps) {
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
// Organiser les tâches par statut
|
||||
const tasksByStatus = useMemo(() => {
|
||||
const grouped = tasks.reduce((acc, task) => {
|
||||
@@ -55,8 +62,34 @@ export function KanbanBoard({ tasks }: KanbanBoardProps) {
|
||||
}
|
||||
];
|
||||
|
||||
const handleCreateTask = async (data: CreateTaskData) => {
|
||||
if (onCreateTask) {
|
||||
await onCreateTask(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-slate-950">
|
||||
{/* Header avec bouton nouvelle tâche */}
|
||||
<div className="flex justify-between items-center p-6 pb-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
|
||||
<h2 className="text-lg font-mono font-bold text-slate-100 uppercase tracking-wider">
|
||||
Kanban Board
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{onCreateTask && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
disabled={loading}
|
||||
>
|
||||
+ Nouvelle tâche
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Board tech dark */}
|
||||
<div className="flex-1 flex gap-6 overflow-x-auto p-6">
|
||||
{columns.map((column) => (
|
||||
@@ -66,9 +99,21 @@ export function KanbanBoard({ tasks }: KanbanBoardProps) {
|
||||
title={column.title}
|
||||
color={column.color}
|
||||
tasks={column.tasks}
|
||||
onCreateTask={onCreateTask ? handleCreateTask : undefined}
|
||||
onDeleteTask={onDeleteTask}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Modal de création */}
|
||||
{onCreateTask && (
|
||||
<CreateTaskForm
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onSubmit={handleCreateTask}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
32
components/kanban/BoardContainer.tsx
Normal file
32
components/kanban/BoardContainer.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { KanbanBoard } from './Board';
|
||||
import { useTasks } from '@/hooks/useTasks';
|
||||
import { Task } from '@/lib/types';
|
||||
|
||||
interface BoardContainerProps {
|
||||
initialTasks: Task[];
|
||||
initialStats: {
|
||||
total: number;
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
todo: number;
|
||||
completionRate: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function KanbanBoardContainer({ initialTasks, initialStats }: BoardContainerProps) {
|
||||
const { tasks, loading, createTask, deleteTask } = useTasks(
|
||||
{ limit: 20 },
|
||||
{ tasks: initialTasks, stats: initialStats }
|
||||
);
|
||||
|
||||
return (
|
||||
<KanbanBoard
|
||||
tasks={tasks}
|
||||
onCreateTask={createTask}
|
||||
onDeleteTask={deleteTask}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
import { Task, TaskStatus } from '@/lib/types';
|
||||
import { TaskCard } from './TaskCard';
|
||||
import { QuickAddTask } from './QuickAddTask';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { CreateTaskData } from '@/clients/tasks-client';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface KanbanColumnProps {
|
||||
id: TaskStatus;
|
||||
title: string;
|
||||
color: string;
|
||||
tasks: Task[];
|
||||
onCreateTask?: (data: CreateTaskData) => Promise<void>;
|
||||
onDeleteTask?: (taskId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
||||
export function KanbanColumn({ id, title, color, tasks, onCreateTask, onDeleteTask }: KanbanColumnProps) {
|
||||
const [showQuickAdd, setShowQuickAdd] = useState(false);
|
||||
// Couleurs tech/cyberpunk
|
||||
const techStyles = {
|
||||
gray: {
|
||||
@@ -62,15 +68,38 @@ export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<Badge variant={badgeVariant} size="sm">
|
||||
{String(tasks.length).padStart(2, '0')}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={badgeVariant} size="sm">
|
||||
{String(tasks.length).padStart(2, '0')}
|
||||
</Badge>
|
||||
{onCreateTask && (
|
||||
<button
|
||||
onClick={() => setShowQuickAdd(true)}
|
||||
className={`w-5 h-5 rounded-full border border-dashed ${style.border} ${style.accent} hover:bg-slate-800/50 transition-colors flex items-center justify-center text-xs font-mono`}
|
||||
title="Ajouter une tâche rapide"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 p-4 h-[calc(100vh-220px)] overflow-y-auto">
|
||||
<div className="space-y-3">
|
||||
{tasks.length === 0 ? (
|
||||
{/* Quick Add Task */}
|
||||
{showQuickAdd && onCreateTask && (
|
||||
<QuickAddTask
|
||||
status={id}
|
||||
onSubmit={async (data) => {
|
||||
await onCreateTask(data);
|
||||
// Ne pas fermer automatiquement pour permettre la création en série
|
||||
}}
|
||||
onCancel={() => setShowQuickAdd(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tasks.length === 0 && !showQuickAdd ? (
|
||||
<div className="text-center py-20">
|
||||
<div className={`w-16 h-16 mx-auto mb-4 rounded-full bg-slate-800 border-2 border-dashed ${style.border} flex items-center justify-center`}>
|
||||
<span className={`text-2xl ${style.accent} opacity-50`}>{techIcons[id]}</span>
|
||||
@@ -82,7 +111,7 @@ export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
||||
</div>
|
||||
) : (
|
||||
tasks.map((task) => (
|
||||
<TaskCard key={task.id} task={task} />
|
||||
<TaskCard key={task.id} task={task} onDelete={onDeleteTask} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
225
components/kanban/QuickAddTask.tsx
Normal file
225
components/kanban/QuickAddTask.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { TaskStatus, TaskPriority } from '@/lib/types';
|
||||
import { CreateTaskData } from '@/clients/tasks-client';
|
||||
|
||||
interface QuickAddTaskProps {
|
||||
status: TaskStatus;
|
||||
onSubmit: (data: CreateTaskData) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function QuickAddTask({ status, onSubmit, onCancel }: QuickAddTaskProps) {
|
||||
const [formData, setFormData] = useState<CreateTaskData>({
|
||||
title: '',
|
||||
description: '',
|
||||
status,
|
||||
priority: 'medium' as TaskPriority,
|
||||
tags: [],
|
||||
dueDate: undefined
|
||||
});
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [activeField, setActiveField] = useState<'title' | 'description' | 'tags' | 'date' | null>('title');
|
||||
const titleRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Focus automatique sur le titre
|
||||
useEffect(() => {
|
||||
titleRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const trimmedTitle = formData.title.trim();
|
||||
console.log('handleSubmit called:', { trimmedTitle, isSubmitting });
|
||||
if (!trimmedTitle || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
console.log('Submitting task:', { ...formData, title: trimmedTitle });
|
||||
await onSubmit({
|
||||
...formData,
|
||||
title: trimmedTitle
|
||||
});
|
||||
|
||||
// Réinitialiser pour la prochaine tâche
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
status,
|
||||
priority: 'medium',
|
||||
tags: [],
|
||||
dueDate: undefined
|
||||
});
|
||||
setTagInput('');
|
||||
setActiveField('title');
|
||||
setIsSubmitting(false);
|
||||
titleRef.current?.focus();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création:', error);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
|
||||
console.log('Key pressed:', e.key, 'field:', field, 'title:', formData.title);
|
||||
|
||||
// Seulement intercepter les touches spécifiques qu'on veut gérer
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (field === 'title' && formData.title.trim()) {
|
||||
console.log('Calling handleSubmit from title');
|
||||
handleSubmit();
|
||||
} else if (field === 'tags' && tagInput.trim()) {
|
||||
console.log('Adding tag');
|
||||
addTag();
|
||||
} else if (formData.title.trim()) {
|
||||
// Permettre création depuis n'importe quel champ si titre rempli
|
||||
console.log('Calling handleSubmit from other field');
|
||||
handleSubmit();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
} else if (e.key === 'Tab' && !e.metaKey && !e.ctrlKey) {
|
||||
// Navigation entre les champs seulement si pas de modificateur
|
||||
e.preventDefault();
|
||||
const fields = ['title', 'description', 'tags', 'date'];
|
||||
const currentIndex = fields.indexOf(activeField || 'title');
|
||||
const nextField = fields[(currentIndex + 1) % fields.length] as typeof activeField;
|
||||
setActiveField(nextField);
|
||||
}
|
||||
// Laisser passer tous les autres événements (y compris les raccourcis système)
|
||||
};
|
||||
|
||||
const addTag = () => {
|
||||
const tag = tagInput.trim();
|
||||
if (tag && !formData.tags?.includes(tag)) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
tags: [...(prev.tags || []), tag]
|
||||
}));
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tagToRemove: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
tags: prev.tags?.filter(tag => tag !== tagToRemove) || []
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBlur = (e: React.FocusEvent) => {
|
||||
// Vérifier si le focus reste dans le composant
|
||||
setTimeout(() => {
|
||||
const currentTarget = e.currentTarget;
|
||||
const relatedTarget = e.relatedTarget as Node | null;
|
||||
|
||||
// Si le focus sort complètement du composant ET qu'il n'y a pas de titre
|
||||
if (currentTarget && (!relatedTarget || !currentTarget.contains(relatedTarget)) && !formData.title.trim()) {
|
||||
onCancel();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<div onBlur={handleBlur}>
|
||||
<Card className="p-3 border-dashed border-cyan-500/30 bg-slate-800/50 hover:border-cyan-500/50 transition-all duration-300">
|
||||
{/* Header avec titre et priorité */}
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<input
|
||||
ref={titleRef}
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
||||
onFocus={() => setActiveField('title')}
|
||||
placeholder="Titre de la tâche..."
|
||||
disabled={isSubmitting}
|
||||
className="flex-1 bg-transparent border-none outline-none text-slate-100 font-mono text-sm font-medium placeholder-slate-500 leading-tight"
|
||||
/>
|
||||
|
||||
{/* Indicateur de priorité */}
|
||||
<select
|
||||
value={formData.priority}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as TaskPriority }))}
|
||||
disabled={isSubmitting}
|
||||
className="bg-transparent border-none outline-none text-xs font-mono text-slate-400"
|
||||
>
|
||||
<option value="low">L</option>
|
||||
<option value="medium">M</option>
|
||||
<option value="high">H</option>
|
||||
<option value="urgent">U</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'description')}
|
||||
onFocus={() => setActiveField('description')}
|
||||
placeholder="Description..."
|
||||
rows={2}
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-transparent border-none outline-none text-xs text-slate-400 font-mono placeholder-slate-500 resize-none mb-2"
|
||||
/>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="mb-2">
|
||||
<div className="flex flex-wrap gap-1 mb-1">
|
||||
{formData.tags && formData.tags.map((tag: string, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="cursor-pointer hover:bg-red-950/50 hover:text-red-300 hover:border-red-500/30 transition-colors"
|
||||
onClick={() => removeTag(tag)}
|
||||
>
|
||||
{tag} ✕
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'tags')}
|
||||
onFocus={() => setActiveField('tags')}
|
||||
placeholder="Tags..."
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-transparent border-none outline-none text-xs text-slate-400 font-mono placeholder-slate-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer avec date et actions */}
|
||||
<div className="pt-2 border-t border-slate-700/50">
|
||||
<div className="flex items-center justify-between text-xs min-w-0">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
dueDate: e.target.value ? new Date(e.target.value) : undefined
|
||||
}))}
|
||||
onFocus={() => setActiveField('date')}
|
||||
disabled={isSubmitting}
|
||||
className="bg-transparent border-none outline-none text-slate-400 font-mono text-xs flex-shrink min-w-0"
|
||||
/>
|
||||
|
||||
{isSubmitting && (
|
||||
<div className="flex items-center gap-1 text-cyan-400 font-mono text-xs flex-shrink-0">
|
||||
<div className="w-3 h-3 border border-cyan-400 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,9 +6,17 @@ import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
onDelete?: (taskId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function TaskCard({ task }: TaskCardProps) {
|
||||
export function TaskCard({ task, onDelete }: TaskCardProps) {
|
||||
const handleDelete = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onDelete) {
|
||||
await onDelete(task.id);
|
||||
}
|
||||
};
|
||||
// Extraire les emojis du titre pour les afficher comme tags visuels
|
||||
const emojiRegex = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu;
|
||||
const emojis = task.title.match(emojiRegex) || [];
|
||||
@@ -33,12 +41,25 @@ export function TaskCard({ task }: TaskCardProps) {
|
||||
{titleWithoutEmojis}
|
||||
</h4>
|
||||
|
||||
{/* Indicateur de priorité tech */}
|
||||
<div className={`w-2 h-2 rounded-full flex-shrink-0 mt-1 animate-pulse ${
|
||||
task.priority === 'high' ? 'bg-red-400 shadow-red-400/50 shadow-sm' :
|
||||
task.priority === 'medium' ? 'bg-yellow-400 shadow-yellow-400/50 shadow-sm' :
|
||||
'bg-slate-500'
|
||||
}`} />
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{/* Bouton de suppression discret */}
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="opacity-0 group-hover:opacity-100 w-4 h-4 rounded-full bg-red-900/50 hover:bg-red-800/80 border border-red-500/30 hover:border-red-400/50 flex items-center justify-center transition-all duration-200 text-red-400 hover:text-red-300 text-xs"
|
||||
title="Supprimer la tâche"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Indicateur de priorité tech */}
|
||||
<div className={`w-2 h-2 rounded-full animate-pulse ${
|
||||
task.priority === 'high' ? 'bg-red-400 shadow-red-400/50 shadow-sm' :
|
||||
task.priority === 'medium' ? 'bg-yellow-400 shadow-yellow-400/50 shadow-sm' :
|
||||
'bg-slate-500'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description tech */}
|
||||
@@ -84,6 +105,12 @@ export function TaskCard({ task }: TaskCardProps) {
|
||||
<span className="text-slate-600 font-mono">--:--</span>
|
||||
)}
|
||||
|
||||
{task.source !== 'manual' && task.source && (
|
||||
<Badge variant="outline" size="sm">
|
||||
{task.source}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{task.completedAt && (
|
||||
<span className="text-emerald-400 font-mono font-bold">✓ DONE</span>
|
||||
)}
|
||||
|
||||
57
components/ui/HeaderContainer.tsx
Normal file
57
components/ui/HeaderContainer.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Header } from './Header';
|
||||
import { tasksClient } from '@/clients/tasks-client';
|
||||
|
||||
interface HeaderContainerProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
initialStats: {
|
||||
total: number;
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
todo: number;
|
||||
completionRate: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function HeaderContainer({ title, subtitle, initialStats }: HeaderContainerProps) {
|
||||
const [stats, setStats] = useState(initialStats);
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
|
||||
// Hydratation côté client
|
||||
useEffect(() => {
|
||||
setIsHydrated(true);
|
||||
}, []);
|
||||
|
||||
// Rafraîchir les stats périodiquement côté client
|
||||
useEffect(() => {
|
||||
if (!isHydrated) return;
|
||||
|
||||
const refreshStats = async () => {
|
||||
try {
|
||||
const response = await tasksClient.getTasks({ limit: 1 }); // Juste pour les stats
|
||||
setStats(response.stats);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du rafraîchissement des stats:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Rafraîchir les stats toutes les 30 secondes
|
||||
const interval = setInterval(refreshStats, 30000);
|
||||
|
||||
// Rafraîchir immédiatement après hydratation
|
||||
refreshStats();
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isHydrated]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
stats={stats}
|
||||
/>
|
||||
);
|
||||
}
|
||||
154
hooks/useTasks.ts
Normal file
154
hooks/useTasks.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { tasksClient, TaskFilters, CreateTaskData, UpdateTaskData } from '@/clients/tasks-client';
|
||||
import { Task } from '@/lib/types';
|
||||
|
||||
interface UseTasksState {
|
||||
tasks: Task[];
|
||||
stats: {
|
||||
total: number;
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
todo: number;
|
||||
completionRate: number;
|
||||
};
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UseTasksActions {
|
||||
refreshTasks: () => Promise<void>;
|
||||
createTask: (data: CreateTaskData) => Promise<Task | null>;
|
||||
updateTask: (data: UpdateTaskData) => Promise<Task | null>;
|
||||
deleteTask: (taskId: string) => Promise<void>;
|
||||
setFilters: (filters: TaskFilters) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook pour la gestion des tâches
|
||||
*/
|
||||
export function useTasks(
|
||||
initialFilters?: TaskFilters,
|
||||
initialData?: { tasks: Task[]; stats: UseTasksState['stats'] }
|
||||
): UseTasksState & UseTasksActions {
|
||||
const [state, setState] = useState<UseTasksState>({
|
||||
tasks: initialData?.tasks || [],
|
||||
stats: initialData?.stats || {
|
||||
total: 0,
|
||||
completed: 0,
|
||||
inProgress: 0,
|
||||
todo: 0,
|
||||
completionRate: 0
|
||||
},
|
||||
loading: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<TaskFilters>(initialFilters || {});
|
||||
|
||||
/**
|
||||
* Récupère les tâches depuis l'API
|
||||
*/
|
||||
const refreshTasks = useCallback(async () => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tasksClient.getTasks(filters);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
tasks: response.data,
|
||||
stats: response.stats,
|
||||
loading: false
|
||||
}));
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||
}));
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
/**
|
||||
* Crée une nouvelle tâche
|
||||
*/
|
||||
const createTask = useCallback(async (data: CreateTaskData): Promise<Task | null> => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tasksClient.createTask(data);
|
||||
|
||||
// Rafraîchir la liste après création
|
||||
await refreshTasks();
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la création'
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
}, [refreshTasks]);
|
||||
|
||||
/**
|
||||
* Met à jour une tâche
|
||||
*/
|
||||
const updateTask = useCallback(async (data: UpdateTaskData): Promise<Task | null> => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tasksClient.updateTask(data);
|
||||
|
||||
// Rafraîchir la liste après mise à jour
|
||||
await refreshTasks();
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour'
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
}, [refreshTasks]);
|
||||
|
||||
/**
|
||||
* Supprime une tâche
|
||||
*/
|
||||
const deleteTask = useCallback(async (taskId: string): Promise<void> => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
await tasksClient.deleteTask(taskId);
|
||||
|
||||
// Rafraîchir la liste après suppression
|
||||
await refreshTasks();
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la suppression'
|
||||
}));
|
||||
throw error; // Re-throw pour que l'UI puisse gérer l'erreur
|
||||
}
|
||||
}, [refreshTasks]);
|
||||
|
||||
// Charger les tâches au montage et quand les filtres changent
|
||||
useEffect(() => {
|
||||
refreshTasks();
|
||||
}, [refreshTasks]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
refreshTasks,
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
setFilters
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Types de base pour les tâches
|
||||
export type TaskStatus = 'todo' | 'in_progress' | 'done' | 'cancelled';
|
||||
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
|
||||
export type TaskSource = 'reminders' | 'jira';
|
||||
export type TaskSource = 'reminders' | 'jira' | 'manual';
|
||||
|
||||
// Interface principale pour les tâches
|
||||
export interface Task {
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import { tasksService } from '@/services/tasks';
|
||||
import { KanbanBoard } from '../../components/kanban/Board';
|
||||
import { Header } from '../../components/ui/Header';
|
||||
import { KanbanBoardContainer } from '@/components/kanban/BoardContainer';
|
||||
import { HeaderContainer } from '@/components/ui/HeaderContainer';
|
||||
|
||||
export default async function HomePage() {
|
||||
// SSR - Récupération des données côté serveur (focus sur les tâches récentes)
|
||||
const [tasks, stats] = await Promise.all([
|
||||
tasksService.getTasks({ limit: 20 }), // Réduire pour voir les nouvelles tâches
|
||||
// SSR - Récupération des données côté serveur
|
||||
const [initialTasks, initialStats] = await Promise.all([
|
||||
tasksService.getTasks({ limit: 20 }),
|
||||
tasksService.getTaskStats()
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950">
|
||||
<Header
|
||||
<HeaderContainer
|
||||
title="TowerControl"
|
||||
subtitle="Gestionnaire de tâches moderne"
|
||||
stats={stats}
|
||||
initialStats={initialStats}
|
||||
/>
|
||||
|
||||
<main className="h-[calc(100vh-120px)]">
|
||||
<KanbanBoard tasks={tasks} />
|
||||
<KanbanBoardContainer
|
||||
initialTasks={initialTasks}
|
||||
initialStats={initialStats}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
"@/*": ["./src/*"],
|
||||
"@/services/*": ["./services/*"],
|
||||
"@/lib/*": ["./lib/*"],
|
||||
"@/components/*": ["./components/*"]
|
||||
"@/components/*": ["./components/*"],
|
||||
"@/clients/*": ["./clients/*"],
|
||||
"@/hooks/*": ["./hooks/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
||||
Reference in New Issue
Block a user