refacto: passing by server actions on taskCard
This commit is contained in:
26
TODO.md
26
TODO.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
|
|||||||
@@ -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
165
src/actions/tasks.ts
Normal 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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user