feat(18-03): create TaskDetailModal component for initiative detail

Controlled dialog showing full task metadata, dependencies, dependents,
and action buttons (Queue Task / Stop Task) with proper enable/disable
logic based on task status and dependency completion.
This commit is contained in:
Lukas May
2026-02-04 21:33:18 +01:00
parent 4becfe8452
commit 5b17b7a93b

View File

@@ -0,0 +1,170 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/StatusBadge";
/** Serialized Task shape as returned by tRPC (Date serialized to string over JSON) */
export interface SerializedTask {
id: string;
planId: string;
name: string;
description: string | null;
type: string;
priority: string;
status: string;
order: number;
createdAt: string;
updatedAt: string;
}
interface DependencyInfo {
name: string;
status: string;
}
interface TaskDetailModalProps {
task: SerializedTask | null;
phaseName: string;
agentName: string | null;
dependencies: DependencyInfo[];
dependents: DependencyInfo[];
onClose: () => void;
onQueueTask: (taskId: string) => void;
onStopTask: (taskId: string) => void;
}
export function TaskDetailModal({
task,
phaseName,
agentName,
dependencies,
dependents,
onClose,
onQueueTask,
onStopTask,
}: TaskDetailModalProps) {
const allDependenciesComplete =
dependencies.length === 0 ||
dependencies.every((d) => d.status === "completed");
const canQueue = task !== null && task.status === "pending" && allDependenciesComplete;
const canStop = task !== null && task.status === "in_progress";
return (
<Dialog open={task !== null} onOpenChange={(open) => { if (!open) onClose(); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>{task?.name ?? "Task"}</DialogTitle>
<DialogDescription>Task details and dependencies</DialogDescription>
</DialogHeader>
{task && (
<div className="space-y-4">
{/* Metadata grid */}
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-muted-foreground">Status</span>
<div className="mt-1">
<StatusBadge status={task.status} />
</div>
</div>
<div>
<span className="text-muted-foreground">Priority</span>
<p className="mt-1 font-medium capitalize">{task.priority}</p>
</div>
<div>
<span className="text-muted-foreground">Phase</span>
<p className="mt-1 font-medium">{phaseName}</p>
</div>
<div>
<span className="text-muted-foreground">Type</span>
<p className="mt-1 font-medium">{task.type}</p>
</div>
<div className="col-span-2">
<span className="text-muted-foreground">Agent</span>
<p className="mt-1 font-medium">
{agentName ?? "Unassigned"}
</p>
</div>
</div>
{/* Description */}
<div>
<h4 className="mb-1 text-sm font-medium">Description</h4>
<p className="text-sm text-muted-foreground">
{task.description ?? "No description"}
</p>
</div>
{/* Dependencies */}
<div>
<h4 className="mb-1 text-sm font-medium">Dependencies</h4>
{dependencies.length === 0 ? (
<p className="text-sm text-muted-foreground">
No dependencies
</p>
) : (
<ul className="space-y-1">
{dependencies.map((dep) => (
<li
key={dep.name}
className="flex items-center gap-2 text-sm"
>
<span>{dep.name}</span>
<StatusBadge status={dep.status} />
</li>
))}
</ul>
)}
</div>
{/* Dependents (Blocks) */}
<div>
<h4 className="mb-1 text-sm font-medium">Blocks</h4>
{dependents.length === 0 ? (
<p className="text-sm text-muted-foreground">None</p>
) : (
<ul className="space-y-1">
{dependents.map((dep) => (
<li
key={dep.name}
className="flex items-center gap-2 text-sm"
>
<span>{dep.name}</span>
<StatusBadge status={dep.status} />
</li>
))}
</ul>
)}
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
size="sm"
disabled={!canQueue}
onClick={() => task && onQueueTask(task.id)}
>
Queue Task
</Button>
<Button
variant="destructive"
size="sm"
disabled={!canStop}
onClick={() => task && onStopTask(task.id)}
>
Stop Task
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}