refacto: passing by server actions on taskCard

This commit is contained in:
Julien Froidefond
2025-09-18 09:37:46 +02:00
parent 228e1563c6
commit 4a4eb9c8ad
15 changed files with 286 additions and 330 deletions

26
TODO.md
View File

@@ -138,23 +138,27 @@
- [ ] Graphiques avec Chart.js ou Recharts - [ ] Graphiques avec Chart.js ou Recharts
- [ ] Export des données en CSV/JSON - [ ] Export des données en CSV/JSON
## Autre Todo
- [ ] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique)
## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau) ## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau)
### 4.1 Migration vers Server Actions - Actions rapides ### 4.1 Migration vers Server Actions - Actions rapides
**Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes **Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes
#### Actions TaskCard (Priorité 1) #### Actions TaskCard (Priorité 1)
- [ ] Créer `actions/tasks.ts` avec server actions de base - [x] Créer `actions/tasks.ts` avec server actions de base
- [ ] `updateTaskStatus(taskId, status)` - Changement de statut - [x] `updateTaskStatus(taskId, status)` - Changement de statut
- [ ] `updateTaskTitle(taskId, title)` - Édition inline du titre - [x] `updateTaskTitle(taskId, title)` - Édition inline du titre
- [ ] `deleteTask(taskId)` - Suppression de tâche - [x] `deleteTask(taskId)` - Suppression de tâche
- [ ] Modifier `TaskCard.tsx` pour utiliser server actions directement - [x] Modifier `TaskCard.tsx` pour utiliser server actions directement
- [ ] Remplacer les props callbacks par calls directs aux actions - [x] Remplacer les props callbacks par calls directs aux actions
- [ ] Intégrer `useTransition` pour les loading states natifs - [x] Intégrer `useTransition` pour les loading states natifs
- [ ] Tester la revalidation automatique du cache - [x] Tester la revalidation automatique du cache
- [ ] **Nettoyage** : Supprimer `PATCH /api/tasks` et `DELETE /api/tasks` - [x] **Nettoyage** : Supprimer props obsolètes dans tous les composants Kanban
- [ ] **Nettoyage** : Simplifier `tasks-client.ts` (garder GET et POST uniquement) - [x] **Nettoyage** : Simplifier `tasks-client.ts` (garder GET et POST uniquement)
- [ ] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions - [x] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions
#### Actions Daily (Priorité 2) #### Actions Daily (Priorité 2)
- [ ] Créer `actions/daily.ts` pour les checkboxes - [ ] Créer `actions/daily.ts` pour les checkboxes

View File

@@ -65,43 +65,8 @@ export class TasksClient {
return httpClient.get<TasksResponse>('/tasks', params); return httpClient.get<TasksResponse>('/tasks', params);
} }
/** // Note: Les méthodes createTask, updateTask et deleteTask ont été migrées vers Server Actions
* Crée une nouvelle tâche // Voir /src/actions/tasks.ts pour createTask, updateTask, updateTaskTitle, updateTaskStatus, deleteTask
*/
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 // Instance singleton

View File

@@ -6,19 +6,26 @@ import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { TagInput } from '@/components/ui/TagInput'; import { TagInput } from '@/components/ui/TagInput';
import { Task, TaskPriority, TaskStatus } from '@/lib/types'; import { Task, TaskPriority, TaskStatus } from '@/lib/types';
import { UpdateTaskData } from '@/clients/tasks-client'; // UpdateTaskData removed - using Server Actions directly
import { getAllStatuses, getAllPriorities } from '@/lib/status-config'; import { getAllStatuses, getAllPriorities } from '@/lib/status-config';
interface EditTaskFormProps { interface EditTaskFormProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (data: UpdateTaskData) => Promise<void>; onSubmit: (data: { taskId: string; title?: string; description?: string; status?: TaskStatus; priority?: TaskPriority; tags?: string[]; dueDate?: Date; }) => Promise<void>;
task: Task | null; task: Task | null;
loading?: boolean; loading?: boolean;
} }
export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false }: EditTaskFormProps) { export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false }: EditTaskFormProps) {
const [formData, setFormData] = useState<Omit<UpdateTaskData, 'taskId'>>({ const [formData, setFormData] = useState<{
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
tags: string[];
dueDate?: Date;
}>({
title: '', title: '',
description: '', description: '',
status: 'todo' as TaskStatus, status: 'todo' as TaskStatus,

View File

@@ -18,15 +18,13 @@ import { TaskCard } from './TaskCard';
interface KanbanBoardProps { interface KanbanBoardProps {
tasks: Task[]; tasks: Task[];
onCreateTask?: (data: CreateTaskData) => Promise<void>; onCreateTask?: (data: CreateTaskData) => Promise<void>;
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean; compactView?: boolean;
visibleStatuses?: TaskStatus[]; visibleStatuses?: TaskStatus[];
} }
export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, onUpdateStatus, compactView = false, visibleStatuses }: KanbanBoardProps) { export function KanbanBoard({ tasks, onCreateTask, onEditTask, onUpdateStatus, compactView = false, visibleStatuses }: KanbanBoardProps) {
const [activeTask, setActiveTask] = useState<Task | null>(null); const [activeTask, setActiveTask] = useState<Task | null>(null);
const { isColumnVisible } = useUserPreferences(); const { isColumnVisible } = useUserPreferences();
const { isMounted, sensors } = useDragAndDrop(); const { isMounted, sensors } = useDragAndDrop();
@@ -95,9 +93,7 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
id={column.id} id={column.id}
tasks={column.tasks} tasks={column.tasks}
onCreateTask={onCreateTask} onCreateTask={onCreateTask}
onDeleteTask={onDeleteTask}
onEditTask={onEditTask} onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView} compactView={compactView}
/> />
))} ))}
@@ -124,9 +120,7 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
<div className="rotate-3 opacity-90"> <div className="rotate-3 opacity-90">
<TaskCard <TaskCard
task={activeTask} task={activeTask}
onDelete={undefined}
onEdit={undefined} onEdit={undefined}
onUpdateTitle={undefined}
/> />
</div> </div>
) : null} ) : null}

View File

@@ -9,8 +9,9 @@ import { KanbanFilters } from './KanbanFilters';
import { EditTaskForm } from '@/components/forms/EditTaskForm'; import { EditTaskForm } from '@/components/forms/EditTaskForm';
import { useTasksContext } from '@/contexts/TasksContext'; import { useTasksContext } from '@/contexts/TasksContext';
import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { Task, TaskStatus } from '@/lib/types'; import { Task, TaskStatus, TaskPriority } from '@/lib/types';
import { UpdateTaskData, CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { updateTask, createTask } from '@/actions/tasks';
import { getAllStatuses } from '@/lib/status-config'; import { getAllStatuses } from '@/lib/status-config';
interface KanbanBoardContainerProps { interface KanbanBoardContainerProps {
@@ -26,13 +27,11 @@ export function KanbanBoardContainer({
filteredTasks, filteredTasks,
pinnedTasks, pinnedTasks,
loading, loading,
createTask,
deleteTask,
updateTask,
updateTaskOptimistic, updateTaskOptimistic,
kanbanFilters, kanbanFilters,
setKanbanFilters, setKanbanFilters,
tags tags,
refreshTasks
} = useTasksContext(); } = useTasksContext();
const { preferences, toggleColumnVisibility, isColumnVisible } = useUserPreferences(); const { preferences, toggleColumnVisibility, isColumnVisible } = useUserPreferences();
@@ -45,24 +44,20 @@ export function KanbanBoardContainer({
setEditingTask(task); setEditingTask(task);
}; };
const handleUpdateTask = async (data: UpdateTaskData) => { const handleUpdateTask = async (data: { taskId: string; title?: string; description?: string; status?: TaskStatus; priority?: TaskPriority; tags?: string[]; dueDate?: Date; }) => {
await updateTask(data); const result = await updateTask(data);
setEditingTask(null); if (result.success) {
await refreshTasks(); // Rafraîchir les données
setEditingTask(null);
} else {
console.error('Error updating task:', result.error);
}
}; };
const handleUpdateTitle = async (taskId: string, newTitle: string) => {
await updateTask({
taskId,
title: newTitle
});
};
const handleUpdateStatus = async (taskId: string, newStatus: TaskStatus) => { const handleUpdateStatus = async (taskId: string, newStatus: TaskStatus) => {
// Utiliser la mise à jour optimiste pour le drag & drop // Utiliser la mise à jour optimiste pour le drag & drop
await updateTaskOptimistic({ await updateTaskOptimistic(taskId, newStatus);
taskId,
status: newStatus
});
}; };
// Obtenir le nom du tag épinglé pour l'affichage // Obtenir le nom du tag épinglé pour l'affichage
@@ -70,7 +65,12 @@ export function KanbanBoardContainer({
// Wrapper pour adapter le type de createTask // Wrapper pour adapter le type de createTask
const handleCreateTask = async (data: CreateTaskData): Promise<void> => { const handleCreateTask = async (data: CreateTaskData): Promise<void> => {
await createTask(data); const result = await createTask(data);
if (result.success) {
await refreshTasks(); // Rafraîchir les données
} else {
console.error('Error creating task:', result.error);
}
}; };
return ( return (
@@ -89,9 +89,7 @@ export function KanbanBoardContainer({
{showObjectives && pinnedTasks.length > 0 && ( {showObjectives && pinnedTasks.length > 0 && (
<ObjectivesBoard <ObjectivesBoard
tasks={pinnedTasks} tasks={pinnedTasks}
onDeleteTask={deleteTask}
onEditTask={handleEditTask} onEditTask={handleEditTask}
onUpdateTitle={handleUpdateTitle}
onUpdateStatus={handleUpdateStatus} onUpdateStatus={handleUpdateStatus}
compactView={kanbanFilters.compactView} compactView={kanbanFilters.compactView}
pinnedTagName={pinnedTagName} pinnedTagName={pinnedTagName}
@@ -103,9 +101,7 @@ export function KanbanBoardContainer({
<PrioritySwimlanesBoard <PrioritySwimlanesBoard
tasks={filteredTasks} tasks={filteredTasks}
onCreateTask={handleCreateTask} onCreateTask={handleCreateTask}
onDeleteTask={deleteTask}
onEditTask={handleEditTask} onEditTask={handleEditTask}
onUpdateTitle={handleUpdateTitle}
onUpdateStatus={handleUpdateStatus} onUpdateStatus={handleUpdateStatus}
compactView={kanbanFilters.compactView} compactView={kanbanFilters.compactView}
visibleStatuses={visibleStatuses} visibleStatuses={visibleStatuses}
@@ -115,9 +111,7 @@ export function KanbanBoardContainer({
<SwimlanesBoard <SwimlanesBoard
tasks={filteredTasks} tasks={filteredTasks}
onCreateTask={handleCreateTask} onCreateTask={handleCreateTask}
onDeleteTask={deleteTask}
onEditTask={handleEditTask} onEditTask={handleEditTask}
onUpdateTitle={handleUpdateTitle}
onUpdateStatus={handleUpdateStatus} onUpdateStatus={handleUpdateStatus}
compactView={kanbanFilters.compactView} compactView={kanbanFilters.compactView}
visibleStatuses={visibleStatuses} visibleStatuses={visibleStatuses}
@@ -128,9 +122,7 @@ export function KanbanBoardContainer({
<KanbanBoard <KanbanBoard
tasks={filteredTasks} tasks={filteredTasks}
onCreateTask={handleCreateTask} onCreateTask={handleCreateTask}
onDeleteTask={deleteTask}
onEditTask={handleEditTask} onEditTask={handleEditTask}
onUpdateTitle={handleUpdateTitle}
onUpdateStatus={handleUpdateStatus} onUpdateStatus={handleUpdateStatus}
compactView={kanbanFilters.compactView} compactView={kanbanFilters.compactView}
visibleStatuses={visibleStatuses} visibleStatuses={visibleStatuses}

View File

@@ -12,13 +12,11 @@ interface KanbanColumnProps {
id: TaskStatus; id: TaskStatus;
tasks: Task[]; tasks: Task[];
onCreateTask?: (data: CreateTaskData) => Promise<void>; onCreateTask?: (data: CreateTaskData) => Promise<void>;
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
compactView?: boolean; compactView?: boolean;
} }
export function KanbanColumn({ id, tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, compactView = false }: KanbanColumnProps) { export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView = false }: KanbanColumnProps) {
const [showQuickAdd, setShowQuickAdd] = useState(false); const [showQuickAdd, setShowQuickAdd] = useState(false);
// Configuration de la zone droppable // Configuration de la zone droppable
@@ -91,7 +89,7 @@ export function KanbanColumn({ id, tasks, onCreateTask, onDeleteTask, onEditTask
</div> </div>
) : ( ) : (
tasks.map((task) => ( tasks.map((task) => (
<TaskCard key={task.id} task={task} onDelete={onDeleteTask} onEdit={onEditTask} onUpdateTitle={onUpdateTitle} compactView={compactView} /> <TaskCard key={task.id} task={task} onEdit={onEditTask} compactView={compactView} />
)) ))
)} )}
</div> </div>

View File

@@ -21,9 +21,7 @@ import { useDroppable } from '@dnd-kit/core';
interface ObjectivesBoardProps { interface ObjectivesBoardProps {
tasks: Task[]; tasks: Task[];
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean; compactView?: boolean;
pinnedTagName?: string; pinnedTagName?: string;
@@ -36,9 +34,7 @@ function DroppableColumn({
title, title,
color, color,
icon, icon,
onDeleteTask,
onEditTask, onEditTask,
onUpdateTitle,
compactView compactView
}: { }: {
status: TaskStatus; status: TaskStatus;
@@ -46,9 +42,7 @@ function DroppableColumn({
title: string; title: string;
color: string; color: string;
icon: string; icon: string;
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
compactView: boolean; compactView: boolean;
}) { }) {
const { setNodeRef } = useDroppable({ const { setNodeRef } = useDroppable({
@@ -80,9 +74,7 @@ function DroppableColumn({
<div key={task.id} className="transform hover:scale-[1.02] transition-transform duration-200"> <div key={task.id} className="transform hover:scale-[1.02] transition-transform duration-200">
<TaskCard <TaskCard
task={task} task={task}
onDelete={onDeleteTask}
onEdit={onEditTask} onEdit={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView} compactView={compactView}
/> />
</div> </div>
@@ -96,9 +88,7 @@ function DroppableColumn({
export function ObjectivesBoard({ export function ObjectivesBoard({
tasks, tasks,
onDeleteTask,
onEditTask, onEditTask,
onUpdateTitle,
onUpdateStatus, onUpdateStatus,
compactView = false, compactView = false,
pinnedTagName = "Objectifs" pinnedTagName = "Objectifs"
@@ -209,9 +199,7 @@ export function ObjectivesBoard({
title="À faire" title="À faire"
color="bg-[var(--primary)]" color="bg-[var(--primary)]"
icon="📋" icon="📋"
onDeleteTask={onDeleteTask}
onEditTask={onEditTask} onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView} compactView={compactView}
/> />
@@ -221,9 +209,7 @@ export function ObjectivesBoard({
title="En cours" title="En cours"
color="bg-yellow-400" color="bg-yellow-400"
icon="🔄" icon="🔄"
onDeleteTask={onDeleteTask}
onEditTask={onEditTask} onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView} compactView={compactView}
/> />
@@ -233,9 +219,7 @@ export function ObjectivesBoard({
title="Terminé" title="Terminé"
color="bg-green-400" color="bg-green-400"
icon="✅" icon="✅"
onDeleteTask={onDeleteTask}
onEditTask={onEditTask} onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView} compactView={compactView}
/> />
</div> </div>
@@ -267,9 +251,7 @@ export function ObjectivesBoard({
<div className="rotate-3 opacity-90"> <div className="rotate-3 opacity-90">
<TaskCard <TaskCard
task={activeTask} task={activeTask}
onDelete={undefined}
onEdit={undefined} onEdit={undefined}
onUpdateTitle={undefined}
compactView={compactView} compactView={compactView}
/> />
</div> </div>

View File

@@ -10,9 +10,7 @@ interface PrioritySwimlanesBoardProps {
loading: boolean; loading: boolean;
tasks: Task[]; tasks: Task[];
onCreateTask?: (data: CreateTaskData) => Promise<void>; onCreateTask?: (data: CreateTaskData) => Promise<void>;
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean; compactView?: boolean;
visibleStatuses?: TaskStatus[]; visibleStatuses?: TaskStatus[];
@@ -21,9 +19,7 @@ interface PrioritySwimlanesBoardProps {
export function PrioritySwimlanesBoard({ export function PrioritySwimlanesBoard({
tasks, tasks,
onCreateTask, onCreateTask,
onDeleteTask,
onEditTask, onEditTask,
onUpdateTitle,
onUpdateStatus, onUpdateStatus,
compactView = false, compactView = false,
visibleStatuses, visibleStatuses,
@@ -66,9 +62,7 @@ export function PrioritySwimlanesBoard({
tasks={tasks} tasks={tasks}
swimlanes={swimlanesData} swimlanes={swimlanesData}
onCreateTask={onCreateTask} onCreateTask={onCreateTask}
onDeleteTask={onDeleteTask}
onEditTask={onEditTask} onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
onUpdateStatus={onUpdateStatus} onUpdateStatus={onUpdateStatus}
compactView={compactView} compactView={compactView}
visibleStatuses={visibleStatuses} visibleStatuses={visibleStatuses}

View File

@@ -25,9 +25,7 @@ import { useDroppable } from '@dnd-kit/core';
function DroppableColumn({ function DroppableColumn({
status, status,
tasks, tasks,
onDeleteTask,
onEditTask, onEditTask,
onUpdateTitle,
compactView, compactView,
onCreateTask, onCreateTask,
showQuickAdd, showQuickAdd,
@@ -36,9 +34,7 @@ function DroppableColumn({
}: { }: {
status: TaskStatus; status: TaskStatus;
tasks: Task[]; tasks: Task[];
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
compactView: boolean; compactView: boolean;
onCreateTask?: (data: CreateTaskData) => Promise<void>; onCreateTask?: (data: CreateTaskData) => Promise<void>;
showQuickAdd?: boolean; showQuickAdd?: boolean;
@@ -60,9 +56,7 @@ function DroppableColumn({
<TaskCard <TaskCard
key={task.id} key={task.id}
task={task} task={task}
onDelete={onDeleteTask}
onEdit={onEditTask} onEdit={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView} compactView={compactView}
/> />
))} ))}
@@ -117,9 +111,7 @@ interface SwimlanesBaseProps {
tasks: Task[]; tasks: Task[];
swimlanes: SwimlaneData[]; swimlanes: SwimlaneData[];
onCreateTask?: (data: CreateTaskData) => Promise<void>; onCreateTask?: (data: CreateTaskData) => Promise<void>;
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean; compactView?: boolean;
visibleStatuses?: TaskStatus[]; visibleStatuses?: TaskStatus[];
@@ -129,9 +121,7 @@ export function SwimlanesBase({
tasks, tasks,
swimlanes, swimlanes,
onCreateTask, onCreateTask,
onDeleteTask,
onEditTask, onEditTask,
onUpdateTitle,
onUpdateStatus, onUpdateStatus,
compactView = false, compactView = false,
visibleStatuses visibleStatuses
@@ -270,9 +260,7 @@ export function SwimlanesBase({
key={columnId} key={columnId}
status={status} status={status}
tasks={statusTasks} tasks={statusTasks}
onDeleteTask={onDeleteTask}
onEditTask={onEditTask} onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView} compactView={compactView}
onCreateTask={onCreateTask ? (data) => handleQuickAdd(data, columnId) : undefined} onCreateTask={onCreateTask ? (data) => handleQuickAdd(data, columnId) : undefined}
showQuickAdd={showQuickAdd[columnId] || false} showQuickAdd={showQuickAdd[columnId] || false}

View File

@@ -9,9 +9,7 @@ import { SwimlanesBase, SwimlaneData } from './SwimlanesBase';
interface SwimlanesboardProps { interface SwimlanesboardProps {
tasks: Task[]; tasks: Task[];
onCreateTask?: (data: CreateTaskData) => Promise<void>; onCreateTask?: (data: CreateTaskData) => Promise<void>;
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean; compactView?: boolean;
visibleStatuses?: TaskStatus[]; visibleStatuses?: TaskStatus[];
@@ -21,9 +19,7 @@ interface SwimlanesboardProps {
export function SwimlanesBoard({ export function SwimlanesBoard({
tasks, tasks,
onCreateTask, onCreateTask,
onDeleteTask,
onEditTask, onEditTask,
onUpdateTitle,
onUpdateStatus, onUpdateStatus,
compactView = false, compactView = false,
visibleStatuses, visibleStatuses,
@@ -88,9 +84,7 @@ export function SwimlanesBoard({
tasks={tasks} tasks={tasks}
swimlanes={swimlanesData} swimlanes={swimlanesData}
onCreateTask={onCreateTask} onCreateTask={onCreateTask}
onDeleteTask={onDeleteTask}
onEditTask={onEditTask} onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
onUpdateStatus={onUpdateStatus} onUpdateStatus={onUpdateStatus}
compactView={compactView} compactView={compactView}
visibleStatuses={visibleStatuses} visibleStatuses={visibleStatuses}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useTransition } from 'react';
import { Task } from '@/lib/types'; import { Task } from '@/lib/types';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { fr } from 'date-fns/locale'; import { fr } from 'date-fns/locale';
@@ -9,21 +9,21 @@ import { useTasksContext } from '@/contexts/TasksContext';
import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { useDraggable } from '@dnd-kit/core'; import { useDraggable } from '@dnd-kit/core';
import { getPriorityConfig, getPriorityColorHex } from '@/lib/status-config'; import { getPriorityConfig, getPriorityColorHex } from '@/lib/status-config';
import { updateTaskTitle, deleteTask } from '@/actions/tasks';
interface TaskCardProps { interface TaskCardProps {
task: Task; task: Task;
onDelete?: (taskId: string) => Promise<void>;
onEdit?: (task: Task) => void; onEdit?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
compactView?: boolean; compactView?: boolean;
} }
export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView = false }: TaskCardProps) { export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
const [isEditingTitle, setIsEditingTitle] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false);
const [editTitle, setEditTitle] = useState(task.title); const [editTitle, setEditTitle] = useState(task.title);
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const [isPending, startTransition] = useTransition();
const timeoutRef = useRef<NodeJS.Timeout | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const { tags: availableTags } = useTasksContext(); const { tags: availableTags, refreshTasks } = useTasksContext();
const { preferences } = useUserPreferences(); const { preferences } = useUserPreferences();
// Helper pour construire l'URL Jira // Helper pour construire l'URL Jira
@@ -61,8 +61,18 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
const handleDelete = async (e: React.MouseEvent) => { const handleDelete = async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (onDelete) {
await onDelete(task.id); if (window.confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) {
startTransition(async () => {
const result = await deleteTask(task.id);
if (!result.success) {
console.error('Error deleting task:', result.error);
// TODO: Afficher une notification d'erreur
} else {
// Rafraîchir les données après suppression réussie
await refreshTasks();
}
});
} }
}; };
@@ -77,7 +87,7 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
const handleTitleClick = (e: React.MouseEvent) => { const handleTitleClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (onUpdateTitle && !isDragging) { if (!isDragging && !isPending) {
setIsEditingTitle(true); setIsEditingTitle(true);
setShowTooltip(false); setShowTooltip(false);
} }
@@ -85,8 +95,19 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
const handleTitleSave = async () => { const handleTitleSave = async () => {
const trimmedTitle = editTitle.trim(); const trimmedTitle = editTitle.trim();
if (trimmedTitle && trimmedTitle !== task.title && onUpdateTitle) { if (trimmedTitle && trimmedTitle !== task.title) {
await onUpdateTitle(task.id, trimmedTitle); startTransition(async () => {
const result = await updateTaskTitle(task.id, trimmedTitle);
if (!result.success) {
console.error('Error updating task title:', result.error);
// Remettre l'ancien titre en cas d'erreur
setEditTitle(task.title);
} else {
// Mettre à jour optimistiquement le titre local
// La Server Action a déjà mis à jour la DB, on synchronise juste l'affichage
task.title = trimmedTitle;
}
});
} }
setIsEditingTitle(false); setIsEditingTitle(false);
setShowTooltip(false); setShowTooltip(false);
@@ -142,7 +163,7 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
onClick={handleTitleClick} onClick={handleTitleClick}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
title={onUpdateTitle ? "Cliquer pour éditer" : undefined} title="Cliquer pour éditer"
> >
{titleWithoutEmojis} {titleWithoutEmojis}
</h4> </h4>
@@ -190,6 +211,8 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
task.status === 'done' ? 'opacity-60' : '' task.status === 'done' ? 'opacity-60' : ''
} ${ } ${
isJiraTask ? 'jira-task' : '' isJiraTask ? 'jira-task' : ''
} ${
isPending ? 'opacity-70 pointer-events-none' : ''
}`} }`}
{...attributes} {...attributes}
{...(isEditingTitle ? {} : listeners)} {...(isEditingTitle ? {} : listeners)}
@@ -231,17 +254,19 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
{!isEditingTitle && onEdit && ( {!isEditingTitle && onEdit && (
<button <button
onClick={handleEdit} onClick={handleEdit}
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded-full bg-[var(--primary)]/20 hover:bg-[var(--primary)]/30 border border-[var(--primary)]/30 hover:border-[var(--primary)]/50 flex items-center justify-center transition-all duration-200 text-[var(--primary)] hover:text-[var(--primary)] text-xs" disabled={isPending}
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded-full bg-[var(--primary)]/20 hover:bg-[var(--primary)]/30 border border-[var(--primary)]/30 hover:border-[var(--primary)]/50 flex items-center justify-center transition-all duration-200 text-[var(--primary)] hover:text-[var(--primary)] text-xs disabled:opacity-50"
title="Modifier la tâche" title="Modifier la tâche"
> >
</button> </button>
)} )}
{!isEditingTitle && onDelete && ( {!isEditingTitle && (
<button <button
onClick={handleDelete} onClick={handleDelete}
className="opacity-0 group-hover:opacity-100 w-5 h-5 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)] hover:text-[var(--destructive)] text-xs" disabled={isPending}
className="opacity-0 group-hover:opacity-100 w-5 h-5 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)] hover:text-[var(--destructive)] text-xs disabled:opacity-50"
title="Supprimer la tâche" title="Supprimer la tâche"
> >
× ×
@@ -270,6 +295,8 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
task.status === 'done' ? 'opacity-60' : '' task.status === 'done' ? 'opacity-60' : ''
} ${ } ${
isJiraTask ? 'jira-task' : '' isJiraTask ? 'jira-task' : ''
} ${
isPending ? 'opacity-70 pointer-events-none' : ''
}`} }`}
{...attributes} {...attributes}
{...(isEditingTitle ? {} : listeners)} {...(isEditingTitle ? {} : listeners)}
@@ -312,7 +339,8 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
{!isEditingTitle && onEdit && ( {!isEditingTitle && onEdit && (
<button <button
onClick={handleEdit} onClick={handleEdit}
className="opacity-0 group-hover:opacity-100 w-4 h-4 rounded-full bg-[var(--primary)]/20 hover:bg-[var(--primary)]/30 border border-[var(--primary)]/30 hover:border-[var(--primary)]/50 flex items-center justify-center transition-all duration-200 text-[var(--primary)] hover:text-[var(--primary)] text-xs" disabled={isPending}
className="opacity-0 group-hover:opacity-100 w-4 h-4 rounded-full bg-[var(--primary)]/20 hover:bg-[var(--primary)]/30 border border-[var(--primary)]/30 hover:border-[var(--primary)]/50 flex items-center justify-center transition-all duration-200 text-[var(--primary)] hover:text-[var(--primary)] text-xs disabled:opacity-50"
title="Modifier la tâche" title="Modifier la tâche"
> >
@@ -320,10 +348,11 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView =
)} )}
{/* Bouton de suppression discret - masqué en mode édition */} {/* Bouton de suppression discret - masqué en mode édition */}
{!isEditingTitle && onDelete && ( {!isEditingTitle && (
<button <button
onClick={handleDelete} onClick={handleDelete}
className="opacity-0 group-hover:opacity-100 w-4 h-4 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)] hover:text-[var(--destructive)] text-xs" disabled={isPending}
className="opacity-0 group-hover:opacity-100 w-4 h-4 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)] hover:text-[var(--destructive)] text-xs disabled:opacity-50"
title="Supprimer la tâche" title="Supprimer la tâche"
> >
× ×

View File

@@ -1,8 +1,9 @@
'use client'; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { tasksClient, TaskFilters, CreateTaskData, UpdateTaskData } from '@/clients/tasks-client'; import { tasksClient, TaskFilters, CreateTaskData } from '@/clients/tasks-client';
import { Task, TaskStats } from '@/lib/types'; import { updateTaskStatus } from '@/actions/tasks';
import { Task, TaskStats, TaskStatus } from '@/lib/types';
interface UseTasksState { interface UseTasksState {
tasks: Task[]; tasks: Task[];
@@ -15,9 +16,7 @@ interface UseTasksState {
interface UseTasksActions { interface UseTasksActions {
refreshTasks: () => Promise<void>; refreshTasks: () => Promise<void>;
createTask: (data: CreateTaskData) => Promise<Task | null>; createTask: (data: CreateTaskData) => Promise<Task | null>;
updateTask: (data: UpdateTaskData) => Promise<Task | null>; updateTaskOptimistic: (taskId: string, status: TaskStatus) => Promise<Task | null>;
updateTaskOptimistic: (data: UpdateTaskData) => Promise<Task | null>;
deleteTask: (taskId: string) => Promise<void>;
setFilters: (filters: TaskFilters) => void; setFilters: (filters: TaskFilters) => void;
} }
@@ -95,35 +94,13 @@ export function useTasks(
} }
}, [refreshTasks]); }, [refreshTasks]);
/** // Note: updateTask et deleteTask ont été migrés vers Server Actions
* Met à jour une tâche // Voir /src/actions/tasks.ts pour updateTaskTitle, updateTaskStatus, deleteTask
*/
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) * Met à jour le statut d'une tâche de manière optimiste (pour drag & drop)
*/ */
const updateTaskOptimistic = useCallback(async (data: UpdateTaskData): Promise<Task | null> => { const updateTaskOptimistic = useCallback(async (taskId: string, status: TaskStatus): Promise<Task | null> => {
const { taskId, ...updates } = data;
// 1. Sauvegarder l'état actuel pour rollback // 1. Sauvegarder l'état actuel pour rollback
const currentTasks = state.tasks; const currentTasks = state.tasks;
const taskToUpdate = currentTasks.find(t => t.id === taskId); const taskToUpdate = currentTasks.find(t => t.id === taskId);
@@ -134,7 +111,7 @@ export function useTasks(
} }
// 2. Mise à jour optimiste immédiate de l'état local // 2. Mise à jour optimiste immédiate de l'état local
const updatedTask = { ...taskToUpdate, ...updates }; const updatedTask = { ...taskToUpdate, status };
const updatedTasks = currentTasks.map(task => const updatedTasks = currentTasks.map(task =>
task.id === taskId ? updatedTask : task task.id === taskId ? updatedTask : task
); );
@@ -162,24 +139,17 @@ export function useTasks(
syncing: true // Indiquer qu'une synchronisation est en cours syncing: true // Indiquer qu'une synchronisation est en cours
})); }));
// 3. Appel API en arrière-plan // 3. Appel Server Action en arrière-plan
try { try {
const response = await tasksClient.updateTask(data); const result = await updateTaskStatus(taskId, status);
// Si l'API retourne des données différentes, on met à jour // Si l'action réussit, la revalidation automatique se charge du reste
if (response.data) { if (result.success) {
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 })); setState(prev => ({ ...prev, syncing: false }));
return result.data as Task;
} else {
throw new Error(result.error || 'Erreur lors de la mise à jour');
} }
return response.data;
} catch (error) { } catch (error) {
// 4. Rollback en cas d'erreur // 4. Rollback en cas d'erreur
setState(prev => ({ setState(prev => ({
@@ -207,26 +177,8 @@ export function useTasks(
} }
}, [state.tasks]); }, [state.tasks]);
/** // Note: deleteTask a été migré vers Server Actions
* Supprime une tâche // Utilisez directement deleteTask depuis /src/actions/tasks.ts dans les composants
*/
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 // Charger les tâches au montage seulement si pas de données initiales
useEffect(() => { useEffect(() => {
@@ -239,9 +191,7 @@ export function useTasks(
...state, ...state,
refreshTasks, refreshTasks,
createTask, createTask,
updateTask,
updateTaskOptimistic, updateTaskOptimistic,
deleteTask,
setFilters setFilters
}; };
} }

165
src/actions/tasks.ts Normal file
View File

@@ -0,0 +1,165 @@
'use server'
import { tasksService } from '@/services/tasks';
import { revalidatePath } from 'next/cache';
import { TaskStatus, TaskPriority } from '@/lib/types';
export type ActionResult<T = unknown> = {
success: boolean;
data?: T;
error?: string;
};
/**
* Server Action pour mettre à jour le statut d'une tâche
*/
export async function updateTaskStatus(
taskId: string,
status: TaskStatus
): Promise<ActionResult> {
try {
const task = await tasksService.updateTask(taskId, { status });
// Revalidation automatique du cache
revalidatePath('/');
revalidatePath('/tasks');
return { success: true, data: task };
} catch (error) {
console.error('Error updating task status:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update task status'
};
}
}
/**
* Server Action pour mettre à jour le titre d'une tâche
*/
export async function updateTaskTitle(
taskId: string,
title: string
): Promise<ActionResult> {
try {
if (!title.trim()) {
return { success: false, error: 'Title cannot be empty' };
}
const task = await tasksService.updateTask(taskId, { title: title.trim() });
// Revalidation automatique du cache
revalidatePath('/');
revalidatePath('/tasks');
return { success: true, data: task };
} catch (error) {
console.error('Error updating task title:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update task title'
};
}
}
/**
* Server Action pour supprimer une tâche
*/
export async function deleteTask(taskId: string): Promise<ActionResult> {
try {
await tasksService.deleteTask(taskId);
// Revalidation automatique du cache
revalidatePath('/');
revalidatePath('/tasks');
return { success: true };
} catch (error) {
console.error('Error deleting task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete task'
};
}
}
/**
* Server Action pour mettre à jour une tâche complète (formulaire d'édition)
*/
export async function updateTask(data: {
taskId: string;
title?: string;
description?: string;
status?: TaskStatus;
priority?: TaskPriority;
tags?: string[];
dueDate?: Date;
}): Promise<ActionResult> {
try {
const updateData: Record<string, unknown> = {};
if (data.title !== undefined) {
if (!data.title.trim()) {
return { success: false, error: 'Title cannot be empty' };
}
updateData.title = data.title.trim();
}
if (data.description !== undefined) updateData.description = data.description.trim();
if (data.status !== undefined) updateData.status = data.status;
if (data.priority !== undefined) updateData.priority = data.priority;
if (data.tags !== undefined) updateData.tags = data.tags;
if (data.dueDate !== undefined) updateData.dueDate = data.dueDate;
const task = await tasksService.updateTask(data.taskId, updateData);
// Revalidation automatique du cache
revalidatePath('/');
revalidatePath('/tasks');
return { success: true, data: task };
} catch (error) {
console.error('Error updating task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update task'
};
}
}
/**
* Server Action pour créer une nouvelle tâche
*/
export async function createTask(data: {
title: string;
description?: string;
status?: TaskStatus;
priority?: TaskPriority;
tags?: string[];
}): Promise<ActionResult> {
try {
if (!data.title.trim()) {
return { success: false, error: 'Title is required' };
}
const task = await tasksService.createTask({
title: data.title.trim(),
description: data.description?.trim() || '',
status: data.status || 'todo',
priority: data.priority || 'medium',
tags: data.tags || []
});
// Revalidation automatique du cache
revalidatePath('/');
revalidatePath('/tasks');
return { success: true, data: task };
} catch (error) {
console.error('Error creating task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create task'
};
}
}

View File

@@ -65,112 +65,8 @@ export async function GET(request: Request) {
} }
} }
/** // POST, PATCH, DELETE methods have been migrated to Server Actions
* API route pour créer une nouvelle tâche // See /src/actions/tasks.ts for:
*/ // - createTask (replaces POST)
export async function POST(request: Request) { // - updateTask, updateTaskStatus, updateTaskTitle (replaces PATCH)
try { // - deleteTask (replaces DELETE)
const body = await request.json();
const { title, description, status, priority, tags, dueDate } = body;
if (!title) {
return NextResponse.json({
success: false,
error: 'Le titre est requis'
}, { status: 400 });
}
const task = await tasksService.createTask({
title,
description,
status: status as TaskStatus,
priority: priority as TaskPriority,
tags,
dueDate: dueDate ? new Date(dueDate) : undefined
});
return NextResponse.json({
success: true,
data: task,
message: 'Tâche créée avec succès'
});
} catch (error) {
console.error('❌ Erreur lors de la création de la tâche:', error);
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}, { status: 500 });
}
}
/**
* API route pour mettre à jour une tâche
*/
export async function PATCH(request: Request) {
try {
const body = await request.json();
const { taskId, ...updates } = body;
if (!taskId) {
return NextResponse.json({
success: false,
error: 'taskId est requis'
}, { status: 400 });
}
// Convertir dueDate si présent
if (updates.dueDate) {
updates.dueDate = new Date(updates.dueDate);
}
const updatedTask = await tasksService.updateTask(taskId, updates);
return NextResponse.json({
success: true,
data: updatedTask,
message: 'Tâche mise à jour avec succès'
});
} catch (error) {
console.error('❌ Erreur lors de la mise à jour de la tâche:', error);
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}, { status: 500 });
}
}
/**
* API route pour supprimer une tâche
*/
export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url);
const taskId = searchParams.get('taskId');
if (!taskId) {
return NextResponse.json({
success: false,
error: 'taskId est requis'
}, { status: 400 });
}
await tasksService.deleteTask(taskId);
return NextResponse.json({
success: true,
message: 'Tâche supprimée avec succès'
});
} catch (error) {
console.error('❌ Erreur lors de la suppression de la tâche:', error);
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}, { status: 500 });
}
}

View File

@@ -5,7 +5,7 @@ import { useTasks } from '@/hooks/useTasks';
import { useTags } from '@/hooks/useTags'; import { useTags } from '@/hooks/useTags';
import { useUserPreferences } from './UserPreferencesContext'; import { useUserPreferences } from './UserPreferencesContext';
import { Task, Tag, TaskStats } from '@/lib/types'; import { Task, Tag, TaskStats } from '@/lib/types';
import { CreateTaskData, UpdateTaskData, TaskFilters } from '@/clients/tasks-client'; import { CreateTaskData, TaskFilters } from '@/clients/tasks-client';
import { KanbanFilters } from '@/components/kanban/KanbanFilters'; import { KanbanFilters } from '@/components/kanban/KanbanFilters';
import { sortTasks, getSortOption, DEFAULT_SORT, createSortKey } from '@/lib/sort-config'; import { sortTasks, getSortOption, DEFAULT_SORT, createSortKey } from '@/lib/sort-config';
@@ -17,9 +17,7 @@ interface TasksContextType {
syncing: boolean; syncing: boolean;
error: string | null; error: string | null;
createTask: (data: CreateTaskData) => Promise<Task | null>; createTask: (data: CreateTaskData) => Promise<Task | null>;
updateTask: (data: UpdateTaskData) => Promise<Task | null>; updateTaskOptimistic: (taskId: string, status: TaskStatus) => Promise<Task | null>;
updateTaskOptimistic: (data: UpdateTaskData) => Promise<Task | null>;
deleteTask: (taskId: string) => Promise<void>;
refreshTasks: () => Promise<void>; refreshTasks: () => Promise<void>;
setFilters: (filters: TaskFilters) => void; setFilters: (filters: TaskFilters) => void;
// Kanban filters // Kanban filters