diff --git a/src/components/KanbanBoard.tsx b/src/components/KanbanBoard.tsx index da4f77c..cc47ab6 100644 --- a/src/components/KanbanBoard.tsx +++ b/src/components/KanbanBoard.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { Task, getTasks, updateTask, createTask } from "@/lib/api"; +import TaskModal from "@/components/TaskModal"; const COLUMNS = [ { key: "draft", label: "Backlog", color: "#737373" }, @@ -29,6 +30,7 @@ export default function KanbanBoard({ projectId }: Props) { const [addingTo, setAddingTo] = useState(null); const [draggedTask, setDraggedTask] = useState(null); const [activeColumn, setActiveColumn] = useState(null); + const [selectedTask, setSelectedTask] = useState(null); const loadTasks = async () => { try { @@ -124,7 +126,9 @@ export default function KanbanBoard({ projectId }: Props) { {colTasks.map((task) => (
setSelectedTask(task)} >
setDraggedTask(task.id)} + onClick={() => setSelectedTask(task)} className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 cursor-grab hover:border-[var(--accent)] transition-colors active:cursor-grabbing" > @@ -254,6 +259,22 @@ export default function KanbanBoard({ projectId }: Props) { ); })}
+ {/* 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); + }} + /> + )}
); } diff --git a/src/components/TaskModal.tsx b/src/components/TaskModal.tsx new file mode 100644 index 0000000..a49ae77 --- /dev/null +++ b/src/components/TaskModal.tsx @@ -0,0 +1,286 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Task, Agent, updateTask, deleteTask, getAgents, getTasks } from "@/lib/api"; + +const STATUSES = [ + { key: "draft", label: "Backlog", color: "#737373" }, + { key: "ready", label: "TODO", color: "#3b82f6" }, + { key: "in_progress", label: "In Progress", color: "#f59e0b" }, + { key: "review", label: "Review", color: "#a855f7" }, + { key: "completed", label: "Done", color: "#22c55e" }, + { key: "blocked", label: "Blocked", color: "#ef4444" }, +]; + +const PRIORITIES = [ + { key: "low", label: "Low", color: "#737373" }, + { key: "medium", label: "Medium", color: "#3b82f6" }, + { key: "high", label: "High", color: "#f59e0b" }, + { key: "critical", label: "Critical", color: "#ef4444" }, +]; + +interface Props { + task: Task; + projectId: string; + onClose: () => void; + onUpdated: (task: Task) => void; + onDeleted: (taskId: string) => void; +} + +export default function TaskModal({ task, projectId, onClose, onUpdated, onDeleted }: Props) { + const [title, setTitle] = useState(task.title); + const [description, setDescription] = useState(task.description || ""); + const [status, setStatus] = useState(task.status); + const [priority, setPriority] = useState(task.priority); + const [assignedAgentId, setAssignedAgentId] = useState(task.assigned_agent_id || ""); + const [agents, setAgents] = useState([]); + const [allTasks, setAllTasks] = useState([]); + const [saving, setSaving] = useState(false); + const [editingDesc, setEditingDesc] = useState(false); + const [editingTitle, setEditingTitle] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); + + useEffect(() => { + getAgents().then(setAgents).catch(() => {}); + getTasks(projectId).then((t) => setAllTasks(t.filter((x) => x.id !== task.id))).catch(() => {}); + }, []); + + const save = async (patch: Partial) => { + setSaving(true); + try { + const updated = await updateTask(task.id, patch); + onUpdated(updated); + } catch (e) { + console.error(e); + } finally { + setSaving(false); + } + }; + + const handleDelete = async () => { + try { + await deleteTask(task.id); + onDeleted(task.id); + onClose(); + } catch (e) { + console.error(e); + } + }; + + const currentStatus = STATUSES.find((s) => s.key === status); + const currentPriority = PRIORITIES.find((p) => p.key === priority); + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+ {editingTitle ? ( + setTitle(e.target.value)} + onBlur={() => { + setEditingTitle(false); + if (title.trim() && title !== task.title) save({ title: title.trim() }); + }} + onKeyDown={(e) => { + if (e.key === "Enter") (e.target as HTMLInputElement).blur(); + if (e.key === "Escape") { setTitle(task.title); setEditingTitle(false); } + }} + className="w-full text-xl font-bold bg-transparent border-b border-[var(--accent)] + outline-none pb-1" + /> + ) : ( +

setEditingTitle(true)} + > + {title} +

+ )} +
ID: {task.id.slice(0, 8)}
+
+ +
+ {/* Left: Description */} +
+
+ Описание + {!editingDesc && ( + + )} +
+ + {editingDesc ? ( +
+