import { useMemo } from "react";
import type { ContractWithRelations, ContractPhase } from "../../lib/api";
// Phase deliverables configuration (mirrors backend phase_guidance.rs)
interface RecommendedFile {
templateId: string;
name: string;
priority: "required" | "recommended" | "optional";
description: string;
}
interface PhaseDeliverables {
phase: ContractPhase;
files: RecommendedFile[];
requiresRepository: boolean;
requiresTasks: boolean;
guidance: string;
}
const PHASE_DELIVERABLES: Record<ContractPhase, PhaseDeliverables> = {
research: {
phase: "research",
files: [
{ templateId: "research-notes", name: "Research Notes", priority: "recommended", description: "Document findings and insights" },
{ templateId: "competitor-analysis", name: "Competitor Analysis", priority: "recommended", description: "Analyze competitors" },
{ templateId: "user-research", name: "User Research", priority: "optional", description: "User interviews and personas" },
],
requiresRepository: false,
requiresTasks: false,
guidance: "Gather information and document findings before moving to Specify.",
},
specify: {
phase: "specify",
files: [
{ templateId: "requirements", name: "Requirements Document", priority: "required", description: "Functional and non-functional requirements" },
{ templateId: "user-stories", name: "User Stories", priority: "recommended", description: "Features from user perspective" },
{ templateId: "acceptance-criteria", name: "Acceptance Criteria", priority: "recommended", description: "Testable conditions for completion" },
],
requiresRepository: false,
requiresTasks: false,
guidance: "Define clear requirements and acceptance criteria.",
},
plan: {
phase: "plan",
files: [
{ templateId: "architecture", name: "Architecture Document", priority: "recommended", description: "System architecture and design" },
{ templateId: "task-breakdown", name: "Task Breakdown", priority: "required", description: "Work broken into tasks" },
{ templateId: "technical-design", name: "Technical Design", priority: "optional", description: "Detailed technical specs" },
],
requiresRepository: true,
requiresTasks: false,
guidance: "Design the solution and create a task breakdown. Configure a repository.",
},
execute: {
phase: "execute",
files: [
{ templateId: "dev-notes", name: "Development Notes", priority: "recommended", description: "Implementation details" },
{ templateId: "test-plan", name: "Test Plan", priority: "optional", description: "Testing strategy" },
{ templateId: "implementation-log", name: "Implementation Log", priority: "optional", description: "Progress log" },
],
requiresRepository: true,
requiresTasks: true,
guidance: "Execute tasks and track implementation progress.",
},
review: {
phase: "review",
files: [
{ templateId: "release-notes", name: "Release Notes", priority: "required", description: "Changes for release" },
{ templateId: "review-checklist", name: "Review Checklist", priority: "recommended", description: "Code and feature review" },
{ templateId: "retrospective", name: "Retrospective", priority: "optional", description: "Project learnings" },
],
requiresRepository: false,
requiresTasks: false,
guidance: "Review work and document the release.",
},
};
interface DeliverableStatus {
templateId: string;
name: string;
priority: "required" | "recommended" | "optional";
description: string;
completed: boolean;
fileId?: string;
actualName?: string;
}
interface PhaseDeliverablesProps {
contract: ContractWithRelations;
onCreateFile?: (templateId: string, suggestedName: string) => void;
}
export function PhaseDeliverablesPanel({ contract, onCreateFile }: PhaseDeliverablesProps) {
const deliverables = PHASE_DELIVERABLES[contract.phase];
// Calculate deliverable status
const fileStatuses = useMemo((): DeliverableStatus[] => {
return deliverables.files.map((rec) => {
// Find matching file by name similarity
const matchedFile = contract.files.find((f) => {
const nameLower = f.name.toLowerCase();
const recLower = rec.name.toLowerCase();
return (
f.contractPhase === contract.phase &&
(nameLower.includes(recLower) || recLower.includes(nameLower) || nameLower.includes(rec.templateId.replace("-", " ")))
);
});
return {
...rec,
completed: !!matchedFile,
fileId: matchedFile?.id,
actualName: matchedFile?.name,
};
});
}, [contract.files, contract.phase, deliverables.files]);
// Check repository status
const hasRepository = contract.repositories.length > 0;
// Check task status
const taskStats = useMemo(() => {
const total = contract.tasks.length;
const done = contract.tasks.filter((t) => t.status === "done" || t.status === "merged").length;
const pending = contract.tasks.filter((t) => t.status === "pending").length;
const running = contract.tasks.filter((t) => ["running", "initializing", "starting"].includes(t.status)).length;
const failed = contract.tasks.filter((t) => t.status === "failed").length;
return { total, done, pending, running, failed };
}, [contract.tasks]);
// Calculate completion percentage
const completionPercent = useMemo(() => {
let completed = 0;
let total = 0;
// Count required and recommended files
fileStatuses.forEach((s) => {
if (s.priority !== "optional") {
total++;
if (s.completed) completed++;
}
});
// Count repository if required
if (deliverables.requiresRepository) {
total++;
if (hasRepository) completed++;
}
// Count tasks if in execute phase
if (deliverables.requiresTasks && taskStats.total > 0) {
total++;
if (taskStats.done === taskStats.total) completed++;
}
return total > 0 ? Math.round((completed / total) * 100) : 100;
}, [fileStatuses, hasRepository, deliverables, taskStats]);
const priorityColors = {
required: "text-red-400",
recommended: "text-yellow-400",
optional: "text-[#555]",
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-mono text-xs text-[#75aafc] uppercase">
Phase Deliverables
</h3>
<div className="flex items-center gap-2">
<div className="w-24 h-1.5 bg-[rgba(117,170,252,0.1)] rounded overflow-hidden">
<div
className={`h-full transition-all duration-300 ${
completionPercent === 100 ? "bg-green-400" : "bg-[#75aafc]"
}`}
style={{ width: `${completionPercent}%` }}
/>
</div>
<span className="font-mono text-[10px] text-[#555]">{completionPercent}%</span>
</div>
</div>
{/* Guidance text */}
<p className="font-mono text-xs text-[#555] italic">{deliverables.guidance}</p>
{/* File deliverables */}
<div className="space-y-2">
{fileStatuses.map((status) => (
<div
key={status.templateId}
className={`flex items-center justify-between p-2 border ${
status.completed
? "border-green-400/20 bg-green-400/5"
: "border-[rgba(117,170,252,0.15)]"
}`}
>
<div className="flex items-center gap-2">
<span
className={`font-mono text-xs ${
status.completed ? "text-green-400" : "text-[#555]"
}`}
>
{status.completed ? "[+]" : "[ ]"}
</span>
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-[#dbe7ff]">
{status.completed ? status.actualName : status.name}
</span>
{!status.completed && (
<span className={`font-mono text-[9px] uppercase ${priorityColors[status.priority]}`}>
{status.priority}
</span>
)}
</div>
<span className="font-mono text-[10px] text-[#555]">
{status.description}
</span>
</div>
</div>
{!status.completed && onCreateFile && (
<button
onClick={() => onCreateFile(status.templateId, status.name)}
className="px-2 py-1 font-mono text-[10px] text-[#75aafc] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors"
>
Create
</button>
)}
</div>
))}
</div>
{/* Repository status */}
{deliverables.requiresRepository && (
<div
className={`flex items-center gap-2 p-2 border ${
hasRepository
? "border-green-400/20 bg-green-400/5"
: "border-[rgba(117,170,252,0.15)]"
}`}
>
<span
className={`font-mono text-xs ${
hasRepository ? "text-green-400" : "text-[#555]"
}`}
>
{hasRepository ? "[+]" : "[ ]"}
</span>
<div>
<span className="font-mono text-xs text-[#dbe7ff]">
Repository Configured
</span>
{!hasRepository && (
<span className="font-mono text-[9px] uppercase text-red-400 ml-2">
required
</span>
)}
</div>
</div>
)}
{/* Task status (execute phase) */}
{deliverables.requiresTasks && (
<div
className={`flex items-center justify-between p-2 border ${
taskStats.total > 0 && taskStats.done === taskStats.total
? "border-green-400/20 bg-green-400/5"
: "border-[rgba(117,170,252,0.15)]"
}`}
>
<div className="flex items-center gap-2">
<span
className={`font-mono text-xs ${
taskStats.total > 0 && taskStats.done === taskStats.total
? "text-green-400"
: "text-[#555]"
}`}
>
{taskStats.total > 0 && taskStats.done === taskStats.total ? "[+]" : "[ ]"}
</span>
<span className="font-mono text-xs text-[#dbe7ff]">
Tasks Completed
</span>
</div>
{taskStats.total > 0 ? (
<span className="font-mono text-[10px] text-[#9bc3ff]">
{taskStats.done}/{taskStats.total}
{taskStats.running > 0 && ` (${taskStats.running} running)`}
{taskStats.failed > 0 && (
<span className="text-red-400"> ({taskStats.failed} failed)</span>
)}
</span>
) : (
<span className="font-mono text-[10px] text-[#555]">No tasks yet</span>
)}
</div>
)}
</div>
);
}