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:
102
packages/web/src/components/PhaseAccordion.tsx
Normal file
102
packages/web/src/components/PhaseAccordion.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user