notes/src/components/KanbanView.tsx

116 lines
4.7 KiB
TypeScript
Raw Normal View History

import { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useVault } from "../App";
import { listTasks, toggleTask, type TaskItem } from "../lib/commands";
type Column = "todo" | "in-progress" | "done";
const COLUMNS: { id: Column; label: string; icon: string }[] = [
{ id: "todo", label: "To Do", icon: "☐" },
{ id: "in-progress", label: "In Progress", icon: "◐" },
{ id: "done", label: "Done", icon: "✓" },
];
/**
* KanbanView Visual board extracted from `- [ ]` / `- [/]` / `- [x]` items across vault.
*/
export function KanbanView() {
const { vaultPath, refreshNotes } = useVault();
const navigate = useNavigate();
const [tasks, setTasks] = useState<TaskItem[]>([]);
const [loading, setLoading] = useState(true);
const [dragItem, setDragItem] = useState<TaskItem | null>(null);
const loadTasks = useCallback(async () => {
if (!vaultPath) return;
setLoading(true);
try {
const items = await listTasks(vaultPath);
setTasks(items);
} catch {
setTasks([]);
}
setLoading(false);
}, [vaultPath]);
useEffect(() => { loadTasks(); }, [loadTasks]);
const handleDrop = async (column: Column) => {
if (!dragItem || dragItem.state === column) return;
try {
await toggleTask(vaultPath, dragItem.source_path, dragItem.line_number, column);
setTasks(prev => prev.map(t =>
t.source_path === dragItem.source_path && t.line_number === dragItem.line_number
? { ...t, state: column }
: t
));
refreshNotes();
} catch (e) {
console.error("Toggle failed:", e);
}
setDragItem(null);
};
const openSource = (task: TaskItem) => {
navigate(`/note/${encodeURIComponent(task.source_path)}`);
};
if (loading) {
return (
<div className="kanban-view">
<div className="kanban-loading">Loading tasks</div>
</div>
);
}
return (
<div className="kanban-view">
<div className="kanban-header">
<h2 className="kanban-title">📋 Task Board</h2>
<span className="kanban-count">{tasks.length} tasks</span>
<button className="kanban-refresh" onClick={loadTasks}></button>
</div>
<div className="kanban-columns">
{COLUMNS.map(col => {
const colTasks = tasks.filter(t => t.state === col.id);
return (
<div
key={col.id}
className={`kanban-column ${dragItem ? "drop-ready" : ""}`}
onDragOver={e => e.preventDefault()}
onDrop={() => handleDrop(col.id)}
>
<div className="kanban-column-header">
<span className="kanban-column-icon">{col.icon}</span>
<span className="kanban-column-label">{col.label}</span>
<span className="badge badge-muted">{colTasks.length}</span>
</div>
<div className="kanban-column-body">
{colTasks.map((task, i) => (
<div
key={`${task.source_path}-${task.line_number}-${i}`}
className="kanban-card"
draggable
onDragStart={() => setDragItem(task)}
onDragEnd={() => setDragItem(null)}
>
<div className="kanban-card-text">{task.text}</div>
<button
className="kanban-card-source"
onClick={() => openSource(task)}
>
{task.source_path.replace(".md", "")}
</button>
</div>
))}
{colTasks.length === 0 && (
<div className="kanban-empty">No tasks</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
}