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