"use client"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Select } from "@/components/ui/select"; import { useListProjectsProjectsGet } from "@/api/generated/projects/projects"; import { useListEmployeesEmployeesGet } from "@/api/generated/org/org"; import { useListTasksTasksGet, useUpdateTaskTasksTaskIdPatch } from "@/api/generated/work/work"; const STATUSES = ["backlog", "ready", "in_progress", "review", "blocked", "done"] as const; export default function KanbanPage() { const projects = useListProjectsProjectsGet(); const projectList = projects.data ?? []; const employees = useListEmployeesEmployeesGet(); const employeeList = useMemo(() => employees.data ?? [], [employees.data]); const [projectId, setProjectId] = useState(""); const [assigneeId, setAssigneeId] = useState(""); const [live, setLive] = useState(false); const tasks = useListTasksTasksGet( { ...(projectId ? { project_id: Number(projectId) } : {}), }, { query: { enabled: true, refetchInterval: live ? 5000 : false, refetchIntervalInBackground: false, }, }, ); const taskList = useMemo(() => tasks.data ?? [], [tasks.data]); const updateTask = useUpdateTaskTasksTaskIdPatch({ mutation: { onSuccess: () => tasks.refetch(), }, }); const employeeNameById = useMemo(() => { const m = new Map(); for (const e of employeeList) { if (e.id != null) m.set(e.id, e.name); } return m; }, [employeeList]); const filtered = useMemo(() => { return taskList.filter((t) => { if (assigneeId && String(t.assignee_employee_id ?? "") !== assigneeId) return false; return true; }); }, [taskList, assigneeId]); const tasksByStatus = useMemo(() => { const map = new Map<(typeof STATUSES)[number], typeof filtered>(); for (const s of STATUSES) map.set(s, []); for (const t of filtered) { const s = (t.status ?? "backlog") as (typeof STATUSES)[number]; (map.get(s) ?? map.get("backlog"))?.push(t); } // stable sort inside each column for (const s of STATUSES) { const arr = map.get(s) ?? []; arr.sort((a, b) => String(a.id ?? 0).localeCompare(String(b.id ?? 0))); } return map; }, [filtered]); return (

Kanban

Board view for tasks (quick triage + status moves).

{tasks.error ? (
{(tasks.error as Error).message}
) : null}
Filters Scope the board.
Live updates
Auto-refresh tasks every 5s on this page.
Showing {filtered.length} / {taskList.length} tasks
{STATUSES.map((status) => ( {status.replaceAll("_", " ")} {tasksByStatus.get(status)?.length ?? 0} tasks {(tasksByStatus.get(status) ?? []).map((t) => (
{t.title}
{t.description ? (
{t.description}
) : null}
#{t.id} · {t.project_id ? `proj ${t.project_id}` : "no project"} {t.assignee_employee_id != null ? ` · assignee ${employeeNameById.get(t.assignee_employee_id) ?? t.assignee_employee_id}` : ""}
))} {(tasksByStatus.get(status) ?? []).length === 0 ? (
No tasks
) : null}
))}
Tip: set Actor ID in the left sidebar so changes are attributed correctly.
); }