- Modified the getTasks call to include limit as undefined, ensuring all tasks are fetched without restriction. - Adjusted the dev.db file, reflecting changes in the database schema or data.
242 lines
6.9 KiB
TypeScript
242 lines
6.9 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, limit: undefined });
|
|
|
|
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 {
|
|
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 seulement si pas de données initiales
|
|
useEffect(() => {
|
|
if (!initialData?.tasks?.length) {
|
|
refreshTasks();
|
|
}
|
|
}, [refreshTasks, initialData?.tasks?.length]);
|
|
|
|
return {
|
|
...state,
|
|
refreshTasks,
|
|
createTask,
|
|
updateTask,
|
|
updateTaskOptimistic,
|
|
deleteTask,
|
|
setFilters
|
|
};
|
|
}
|