feat(18-02): create PhaseAccordion component

Expandable/collapsible phase container with chevron toggle, phase
number + name header, task count (completed/total), StatusBadge,
phase-level DependencyIndicator, and TaskRow list when expanded.
This commit is contained in:
Lukas May
2026-02-04 21:33:20 +01:00
parent 5b17b7a93b
commit 92d4d36421

View File

@@ -0,0 +1,102 @@
import { useState } from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { StatusBadge } from "@/components/StatusBadge";
import { DependencyIndicator } from "@/components/DependencyIndicator";
import { TaskRow, type SerializedTask } from "@/components/TaskRow";
/** Phase shape as returned by tRPC (Date fields serialized to string over JSON) */
interface SerializedPhase {
id: string;
initiativeId: string;
number: number;
name: string;
description: string | null;
status: string;
createdAt: string;
updatedAt: string;
}
/** Task entry with associated metadata, pre-assembled by the parent page */
interface TaskEntry {
task: SerializedTask;
agentName: string | null;
blockedBy: Array<{ name: string; status: string }>;
}
interface PhaseAccordionProps {
phase: SerializedPhase;
tasks: TaskEntry[];
defaultExpanded: boolean;
phaseDependencies: Array<{ name: string; status: string }>;
onTaskClick: (taskId: string) => void;
}
export function PhaseAccordion({
phase,
tasks,
defaultExpanded,
phaseDependencies,
onTaskClick,
}: PhaseAccordionProps) {
const [expanded, setExpanded] = useState(defaultExpanded);
const completedCount = tasks.filter(
(t) => t.task.status === "completed",
).length;
const totalCount = tasks.length;
return (
<div className="border-b border-border">
{/* Phase header — clickable to toggle */}
<button
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-accent/50"
onClick={() => setExpanded((prev) => !prev)}
>
{/* Expand / collapse chevron */}
{expanded ? (
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
)}
{/* Phase number + name */}
<span className="min-w-0 flex-1 truncate font-medium">
Phase {phase.number}: {phase.name}
</span>
{/* Task count */}
<span className="shrink-0 text-sm text-muted-foreground">
({completedCount}/{totalCount})
</span>
{/* Phase status */}
<StatusBadge status={phase.status} className="shrink-0" />
</button>
{/* Phase-level dependency indicator (when blocked by another phase) */}
{phaseDependencies.length > 0 && (
<DependencyIndicator
blockedBy={phaseDependencies}
type="phase"
className="pb-2 pl-11"
/>
)}
{/* Expanded task list */}
{expanded && (
<div className="pb-3 pl-10 pr-4">
{tasks.map((entry, idx) => (
<TaskRow
key={entry.task.id}
task={entry.task}
agentName={entry.agentName}
blockedBy={entry.blockedBy}
isLast={idx === tasks.length - 1}
onClick={() => onTaskClick(entry.task.id)}
/>
))}
</div>
)}
</div>
);
}