Files
towercontrol/hooks/useTasks.ts
Julien Froidefond edbd82e8ac refactor: simplify BoardContainer and update task management
- Removed initialTasks and initialStats props from KanbanBoardContainer, now using TasksContext for task management.
- Updated useTasks hook to include a simulated delay for sync indicator during task updates.
- Replaced KanbanBoardContainer with HomePageClient in the HomePage component for a cleaner structure.
2025-09-14 09:13:22 +02:00

243 lines
7.0 KiB
TypeScript

'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;
syncing: boolean; // Pour indiquer les opérations optimistes en cours
}
interface UseTasksActions {
refreshTasks: () => Promise<void>;
createTask: (data: CreateTaskData) => Promise<Task | null>;
updateTask: (data: UpdateTaskData) => Promise<Task | null>;
updateTaskOptimistic: (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,
syncing: false
});
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]);
/**
* Met à jour une tâche de manière optimiste (pour drag & drop)
*/
const updateTaskOptimistic = useCallback(async (data: UpdateTaskData): Promise<Task | null> => {
const { taskId, ...updates } = data;
// 1. Sauvegarder l'état actuel pour rollback
const currentTasks = state.tasks;
const taskToUpdate = currentTasks.find(t => t.id === taskId);
if (!taskToUpdate) {
console.error('Tâche non trouvée pour mise à jour optimiste:', taskId);
return null;
}
// 2. Mise à jour optimiste immédiate de l'état local
const updatedTask = { ...taskToUpdate, ...updates };
const updatedTasks = currentTasks.map(task =>
task.id === taskId ? updatedTask : task
);
// Recalculer les stats
const newStats = {
total: updatedTasks.length,
completed: updatedTasks.filter(t => t.status === 'done').length,
inProgress: updatedTasks.filter(t => t.status === 'in_progress').length,
todo: updatedTasks.filter(t => t.status === 'todo').length,
completionRate: updatedTasks.length > 0
? Math.round((updatedTasks.filter(t => t.status === 'done').length / updatedTasks.length) * 100)
: 0
};
setState(prev => ({
...prev,
tasks: updatedTasks,
stats: newStats,
error: null,
syncing: true // Indiquer qu'une synchronisation est en cours
}));
// 3. Appel API en arrière-plan
try {
// Délai artificiel pour voir l'indicateur de sync (à supprimer en prod)
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await tasksClient.updateTask(data);
// Si l'API retourne des données différentes, on met à jour
if (response.data) {
setState(prev => ({
...prev,
tasks: prev.tasks.map(task =>
task.id === taskId ? response.data : task
),
syncing: false // Synchronisation terminée
}));
} else {
setState(prev => ({ ...prev, syncing: false }));
}
return response.data;
} catch (error) {
// 4. Rollback en cas d'erreur
setState(prev => ({
...prev,
tasks: currentTasks,
stats: {
total: currentTasks.length,
completed: currentTasks.filter(t => t.status === 'done').length,
inProgress: currentTasks.filter(t => t.status === 'in_progress').length,
todo: currentTasks.filter(t => t.status === 'todo').length,
completionRate: currentTasks.length > 0
? Math.round((currentTasks.filter(t => t.status === 'done').length / currentTasks.length) * 100)
: 0
},
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour',
syncing: false // Arrêter l'indicateur de synchronisation
}));
console.error('Erreur lors de la mise à jour optimiste:', error);
return null;
}
}, [state.tasks]);
/**
* 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,
updateTaskOptimistic,
deleteTask,
setFilters
};
}