"use client"; import { CATEGORIES, STATUSES } from "@/lib/constants"; import { KanbanView, Task, TaskCategory, TaskStatus } from "@/types"; import { closestCorners, DndContext, DragEndEvent, DragOverEvent, DragOverlay, DragStartEvent, PointerSensor, useDroppable, useSensor, useSensors, } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useState } from "react"; import Confetti from "./Confetti"; import ConfirmModal from "./ConfirmModal"; import TaskDetailModal from "./TaskDetailModal"; import Toast, { ToastData } from "./Toast"; interface KanbanBoardProps { onBack: () => void; } export default function KanbanBoard({ onBack }: KanbanBoardProps) { const [tasks, setTasks] = useState([]); const [view, setView] = useState("category"); const [addingTask, setAddingTask] = useState(false); const [newTask, setNewTask] = useState({ text: "", category: "urgent" as TaskCategory, }); const [loading, setLoading] = useState(true); const [showConfetti, setShowConfetti] = useState(false); const [showArchived, setShowArchived] = useState(false); const [activeTask, setActiveTask] = useState(null); const [originalTask, setOriginalTask] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const [toasts, setToasts] = useState([]); const [confirmModal, setConfirmModal] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; }>({ isOpen: false, title: "", message: "", onConfirm: () => {} }); const addToast = useCallback( (message: string, type: ToastData["type"] = "success") => { const id = Date.now().toString(); setToasts((prev) => [...prev, { id, message, type }]); }, [] ); const removeToast = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ); useEffect(() => { fetchTasks(); }, []); const fetchTasks = async () => { try { const response = await fetch("/api/tasks"); const data = await response.json(); setTasks(data); } catch (error) { console.error("Error fetching tasks:", error); } finally { setLoading(false); } }; const addTask = async () => { if (!newTask.text.trim()) return; try { const response = await fetch("/api/tasks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newTask), }); if (response.ok) { const createdTask = await response.json(); setTasks([createdTask, ...tasks]); setNewTask({ text: "", category: "urgent" }); setAddingTask(false); addToast("Tâche créée", "success"); } } catch (error) { console.error("Error adding task:", error); addToast("Erreur lors de la création", "error"); } }; const updateTaskStatus = async (taskId: number, status: TaskStatus) => { try { const response = await fetch("/api/tasks", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: taskId, status }), }); if (response.ok) { const updatedTask = await response.json(); setTasks(tasks.map((t) => (t.id === taskId ? updatedTask : t))); } } catch (error) { console.error("Error updating task:", error); } }; const updateTaskCategory = async ( taskId: number, category: TaskCategory ) => { console.log("updateTaskCategory called:", { taskId, category }); try { const response = await fetch("/api/tasks", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: taskId, category }), }); console.log("Response status:", response.status); if (response.ok) { const updatedTask = await response.json(); console.log("Updated task:", updatedTask); setTasks(tasks.map((t) => (t.id === taskId ? updatedTask : t))); } else { const error = await response.json(); console.error("API Error:", error); } } catch (error) { console.error("Error updating task:", error); } }; const completeAndArchiveTask = async (taskId: number) => { try { const response = await fetch("/api/tasks", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: taskId, status: "archived" }), }); if (response.ok) { setShowConfetti(true); setTimeout(() => setShowConfetti(false), 3000); const updatedTask = await response.json(); setTasks(tasks.map((t) => (t.id === taskId ? updatedTask : t))); addToast("Tâche archivée 🎉", "success"); } } catch (error) { console.error("Error archiving task:", error); addToast("Erreur lors de l'archivage", "error"); } }; const restoreTask = async (taskId: number) => { try { const response = await fetch("/api/tasks", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: taskId, status: "todo" }), }); if (response.ok) { const updatedTask = await response.json(); setTasks(tasks.map((t) => (t.id === taskId ? updatedTask : t))); addToast("Tâche restaurée", "info"); } } catch (error) { console.error("Error restoring task:", error); addToast("Erreur lors de la restauration", "error"); } }; const deleteTask = async (taskId: number) => { setConfirmModal({ isOpen: true, title: "Supprimer la tâche", message: "Es-tu sûr de vouloir supprimer cette tâche ? Cette action est irréversible.", onConfirm: async () => { try { const response = await fetch(`/api/tasks?id=${taskId}`, { method: "DELETE", }); if (response.ok) { setTasks(tasks.filter((t) => t.id !== taskId)); addToast("Tâche supprimée", "success"); } } catch (error) { console.error("Error deleting task:", error); addToast("Erreur lors de la suppression", "error"); } setConfirmModal((prev) => ({ ...prev, isOpen: false })); }, }); }; const handleDragStart = (event: DragStartEvent) => { const { active } = event; const task = tasks.find((t) => t.id === active.id); if (task) { setActiveTask(task); setOriginalTask({ ...task }); } }; const handleDragOver = (event: DragOverEvent) => { const { active, over } = event; if (!over) return; const activeId = active.id as number; const overId = over.id; if (view === "category") { const categories = CATEGORIES.map((c) => c.key); let targetCategory: TaskCategory | null = null; if (categories.includes(overId as TaskCategory)) { targetCategory = overId as TaskCategory; } else { const overTask = tasks.find((t) => t.id === overId); if (overTask) { targetCategory = overTask.category; } } if (targetCategory) { const activeTask = tasks.find((t) => t.id === activeId); if (activeTask && activeTask.category !== targetCategory) { setTasks( tasks.map((t) => t.id === activeId ? { ...t, category: targetCategory as TaskCategory, } : t ) ); } } } else { const statuses = STATUSES.map((s) => s.key); let targetStatus: TaskStatus | null = null; if (statuses.includes(overId as TaskStatus)) { targetStatus = overId as TaskStatus; } else { const overTask = tasks.find((t) => t.id === overId); if (overTask) { targetStatus = overTask.status; } } if (targetStatus) { const activeTask = tasks.find((t) => t.id === activeId); if (activeTask && activeTask.status !== targetStatus) { setTasks( tasks.map((t) => t.id === activeId ? { ...t, status: targetStatus as TaskStatus } : t ) ); } } } }; const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; console.log("handleDragEnd:", { activeId: active.id, overId: over?.id, originalTask, }); setActiveTask(null); if (!over || !originalTask) { console.log("No over or originalTask, aborting"); setOriginalTask(null); return; } const activeId = active.id as number; const overId = over.id; if (view === "category") { const categories = CATEGORIES.map((c) => c.key); let targetCategory: TaskCategory | null = null; if (categories.includes(overId as TaskCategory)) { targetCategory = overId as TaskCategory; } else { const overTask = tasks.find((t) => t.id === overId); if (overTask) { targetCategory = overTask.category; } } console.log( "Target category:", targetCategory, "Original:", originalTask.category ); if (targetCategory && originalTask.category !== targetCategory) { console.log( "Calling updateTaskCategory:", activeId, targetCategory ); await updateTaskCategory(activeId, targetCategory); } } else { const statuses = STATUSES.map((s) => s.key); let targetStatus: TaskStatus | null = null; if (statuses.includes(overId as TaskStatus)) { targetStatus = overId as TaskStatus; } else { const overTask = tasks.find((t) => t.id === overId); if (overTask) { targetStatus = overTask.status; } } if (targetStatus) { if (targetStatus === "done") { await completeAndArchiveTask(activeId); } else if (originalTask.status !== targetStatus) { await updateTaskStatus(activeId, targetStatus); } } } setOriginalTask(null); }; const activeTasks = tasks.filter((t) => t.status !== "archived"); const archivedTasks = tasks.filter((t) => t.status === "archived"); const getTasksByCategory = (category: TaskCategory) => activeTasks.filter((t) => t.category === category); const getTasksByStatus = (status: TaskStatus) => activeTasks.filter((t) => t.status === status); if (loading) { return (
Chargement...
); } return ( <>
← Retour

Ton Kanban

setView( view === "category" ? "status" : "category" ) } className="bg-gray-700 hover:bg-gray-600 px-6 py-3 rounded-xl transition-colors" > Vue:{" "} {view === "category" ? "Catégorie" : "Statut"} setShowArchived(!showArchived)} className={`px-6 py-3 rounded-xl transition-colors ${ showArchived ? "bg-slate-600 hover:bg-slate-500" : "bg-gray-700 hover:bg-gray-600" }`} > 📦 Archivées ({archivedTasks.length}) setAddingTask(true)} className="bg-blue-500 hover:bg-blue-600 px-6 py-3 rounded-xl transition-colors" > + Ajouter
💡 Glisse-dépose les tâches entre les colonnes
{showArchived && archivedTasks.length > 0 && (

📦 Tâches archivées

{archivedTasks.map((task) => { const cat = CATEGORIES.find( (c) => c.key === task.category ); return (

{task.text}

{cat?.label} •{" "} {task.added}

); })}
)}
{view === "category" ? ( ) : ( )} {activeTask ? ( ) : null} {addingTask && ( { setAddingTask(false); setNewTask({ text: "", category: "urgent" }); }} /> )} {selectedTask && ( setSelectedTask(null)} onUpdate={(updatedTask) => { setTasks( tasks.map((t) => t.id === updatedTask.id ? updatedTask : t ) ); setSelectedTask(updatedTask); }} onDelete={(taskId) => { deleteTask(taskId); setSelectedTask(null); }} onComplete={(taskId) => { completeAndArchiveTask(taskId); setSelectedTask(null); }} /> )} setConfirmModal((prev) => ({ ...prev, isOpen: false, })) } />
); } function DroppableColumn({ id, children, color, label, count, }: { id: string; children: React.ReactNode; color: string; label: string; count: number; }) { const { setNodeRef, isOver } = useDroppable({ id }); return (
{label} ({count})
{children}
); } function SortableTaskCard({ task, onUpdateStatus, onComplete, onDelete, onOpenDetail, }: { task: Task; onUpdateStatus: (taskId: number, status: TaskStatus) => void; onComplete: (taskId: number) => void; onDelete: (taskId: number) => void; onOpenDetail: (task: Task) => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: task.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; const cat = CATEGORIES.find((c) => c.key === task.category); const checklistCount = task.checklist?.length || 0; const checklistDone = task.checklist?.filter((i) => i.checked).length || 0; return (
onOpenDetail(task)} >
e.stopPropagation()} className="cursor-grab active:cursor-grabbing mb-2 text-gray-500 hover:text-gray-300 flex items-center gap-2" > Glisser

{task.text}

{task.description && ( 📝 Description )} {checklistCount > 0 && ( ☑️ {checklistDone}/{checklistCount} )}
e.stopPropagation()} > {STATUSES.map((status) => ( ))}

{task.added}

); } function TaskCardOverlay({ task }: { task: Task }) { const cat = CATEGORIES.find((c) => c.key === task.category); return (

{task.text}

{task.added}

); } function CategoryView({ tasks, getTasksByCategory, updateTaskStatus, completeAndArchiveTask, deleteTask, onOpenDetail, }: { tasks: Task[]; getTasksByCategory: (category: TaskCategory) => Task[]; updateTaskStatus: (taskId: number, status: TaskStatus) => void; completeAndArchiveTask: (taskId: number) => void; deleteTask: (taskId: number) => void; onOpenDetail: (task: Task) => void; }) { return (
{CATEGORIES.map((cat) => { const categoryTasks = getTasksByCategory(cat.key); return ( t.id)} strategy={verticalListSortingStrategy} > {categoryTasks.map((task) => ( ))} ); })}
); } function StatusView({ tasks, getTasksByStatus, updateTaskStatus, completeAndArchiveTask, deleteTask, onOpenDetail, }: { tasks: Task[]; getTasksByStatus: (status: TaskStatus) => Task[]; updateTaskStatus: (taskId: number, status: TaskStatus) => void; completeAndArchiveTask: (taskId: number) => void; deleteTask: (taskId: number) => void; onOpenDetail: (task: Task) => void; }) { return (
{STATUSES.map((status) => { const statusTasks = getTasksByStatus(status.key); return ( t.id)} strategy={verticalListSortingStrategy} > {statusTasks.map((task) => ( ))} ); })}
); } function AddTaskModal({ newTask, setNewTask, onAdd, onCancel, }: { newTask: { text: string; category: TaskCategory }; setNewTask: (task: { text: string; category: TaskCategory }) => void; onAdd: () => void; onCancel: () => void; }) { return ( e.stopPropagation()} className="bg-gray-800 rounded-2xl p-6 w-full max-w-md" >

Nouvelle tâche