Files
kandev/apps/web/components/task/simple/subtask-stepper.tsx
T
2026-07-31 10:55:29 +01:00

81 lines
3.0 KiB
TypeScript

"use client";
import Link from "@/components/routing/app-link";
import { Badge } from "@kandev/ui/badge";
import { StatusIcon } from "@/app/office/tasks/[id]/status-icon";
import { topoSort } from "./workflow-sort";
import type { Task } from "@/app/office/tasks/[id]/types";
import type { TreeHold } from "@/lib/api/domains/tree-api";
type SubtaskStepperProps = {
items: Task["children"];
activeHold: TreeHold | null;
};
export function SubtaskStepper({ items, activeHold }: SubtaskStepperProps) {
if (items.length === 0) return null;
const sorted = topoSort(items);
const completedIds = new Set(
items
.filter((i) => {
const s = i.status?.toLowerCase();
return s === "done" || s === "completed" || s === "cancelled";
})
.map((i) => i.id),
);
const holdLabel = activeHold?.mode === "pause" ? "Paused" : "Cancelled (tree)";
return (
<div className="mt-8" data-testid="subtask-stepper">
<h2 className="text-sm font-semibold mb-4">Sub-tasks</h2>
<div className="relative">
<div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
<div className="space-y-1 pl-10">
{sorted.map((child, idx) => {
const pendingBlockers = (child.blockedBy ?? []).filter((b) => !completedIds.has(b));
const isBlocked = pendingBlockers.length > 0;
let dotClass = "bg-background border-border";
if (completedIds.has(child.id)) {
dotClass = "bg-primary border-primary text-primary-foreground";
} else if (isBlocked) {
dotClass = "bg-muted border-muted-foreground/30";
}
return (
<div key={child.id} className="relative">
<div
className={`absolute -left-6 top-1/2 -translate-y-1/2 h-4 w-4 rounded-full border-2 flex items-center justify-center text-[10px] font-bold ${dotClass}`}
>
{completedIds.has(child.id) ? "✓" : idx + 1}
</div>
<Link
href={`/office/tasks/${child.id}`}
className={`flex cursor-pointer items-center gap-2 rounded-md px-4 py-2.5 text-sm transition-colors hover:bg-muted/50${isBlocked ? " opacity-60" : ""}`}
>
<StatusIcon status={child.status} className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs text-muted-foreground font-mono shrink-0">
{child.identifier}
</span>
<span className="flex-1 truncate">{child.title}</span>
{isBlocked && (
<Badge variant="outline" className="text-xs shrink-0">
Blocked
</Badge>
)}
{activeHold && !isBlocked && (
<Badge variant="outline" className="text-xs shrink-0">
{holdLabel}
</Badge>
)}
</Link>
</div>
);
})}
</div>
</div>
</div>
);
}