import { useEffect, useState } from "react"; import type { Task } from "@/lib/api"; import { getTasks, updateTask } from "@/lib/api"; import TaskModal from "@/components/TaskModal"; import CreateTaskModal from "@/components/CreateTaskModal"; import { wsClient } from "@/lib/ws"; const COLUMNS = [ { key: "backlog", label: "Backlog", color: "#737373" }, { key: "todo", label: "TODO", color: "#3b82f6" }, { key: "in_progress", label: "In Progress", color: "#f59e0b" }, { key: "in_review", label: "Review", color: "#a855f7" }, { key: "done", label: "Done", color: "#22c55e" }, ]; const PRIORITY_COLORS: Record = { critical: "#ef4444", high: "#f59e0b", medium: "#3b82f6", low: "#737373", }; interface Props { projectId: string; projectSlug: string; } export default function KanbanBoard({ projectId, projectSlug }: Props) { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [draggedTask, setDraggedTask] = useState(null); const [activeColumn, setActiveColumn] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const [createInStatus, setCreateInStatus] = useState(null); const prefix = projectSlug.slice(0, 2).toUpperCase(); const loadTasks = async () => { try { const data = await getTasks(projectId); setTasks(data); } finally { setLoading(false); } }; useEffect(() => { loadTasks(); // Subscribe to WebSocket events for real-time updates const unsubscribeCreated = wsClient.on("task.created", (data: any) => { if (data.project_id === projectId) { setTasks((prev) => [...prev, data as Task]); } }); const unsubscribeUpdated = wsClient.on("task.updated", (data: any) => { if (data.project_id === projectId) { setTasks((prev) => prev.map((t) => t.id === data.id ? { ...t, ...data } : t)); } }); const unsubscribeAssigned = wsClient.on("task.assigned", (data: any) => { if (data.project_id === projectId) { setTasks((prev) => prev.map((t) => t.id === data.id ? { ...t, assignee_slug: data.assignee_slug, assigned_at: data.assigned_at } : t)); } }); return () => { unsubscribeCreated?.(); unsubscribeUpdated?.(); unsubscribeAssigned?.(); }; }, [projectId]); const handleDrop = async (status: string) => { if (!draggedTask) return; const task = tasks.find((t) => t.id === draggedTask); if (!task || task.status === status) return; setTasks((prev) => prev.map((t) => (t.id === draggedTask ? { ...t, status } : t))); setDraggedTask(null); try { await updateTask(draggedTask, { status }); } catch { loadTasks(); } }; const handleMoveTask = async (taskId: string, newStatus: string) => { setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t))); try { await updateTask(taskId, { status: newStatus }); } catch { loadTasks(); } }; if (loading) { return
Загрузка...
; } const TaskCard = ({ task, showMoveButtons, colIndex }: { task: Task; showMoveButtons?: boolean; colIndex?: number }) => (
setDraggedTask(task.id)} onClick={() => setSelectedTask(task)} className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 cursor-pointer hover:border-[var(--accent)] transition-colors" >
{prefix}-{task.number} {task.assignee_slug && ( → {task.assignee_slug} )}
{task.title}
{showMoveButtons && colIndex !== undefined && (
{colIndex > 0 && ( )} {colIndex < COLUMNS.length - 1 && ( )}
)}
); return (
{/* Mobile column tabs */}
{COLUMNS.map((col) => { const count = tasks.filter((t) => t.status === col.key).length; return ( ); })}
{/* Mobile: single column */}
{(() => { const col = COLUMNS.find((c) => c.key === (activeColumn || "backlog"))!; const colTasks = tasks.filter((t) => t.status === col.key); const colIndex = COLUMNS.findIndex((c) => c.key === col.key); return (
{colTasks.map((task) => ( ))}
); })()}
{/* Desktop: horizontal kanban */}
{COLUMNS.map((col) => { const colTasks = tasks.filter((t) => t.status === col.key); return (
e.preventDefault()} onDrop={() => handleDrop(col.key)}>
{col.label} {colTasks.length}
{colTasks.map((task) => ( ))}
); })}
{/* Task modal */} {selectedTask && ( setSelectedTask(null)} onUpdated={(updated) => { setTasks((prev) => prev.map((t) => (t.id === updated.id ? updated : t))); setSelectedTask(updated); }} onDeleted={(id) => { setTasks((prev) => prev.filter((t) => t.id !== id)); setSelectedTask(null); }} /> )} {/* Create task modal */} {createInStatus && ( setCreateInStatus(null)} onCreated={(task) => setTasks((prev) => [...prev, task])} /> )}
); }