import { useMemo } from "react";
import type { ContractWithRelations, ContractPhase, ContractType } from "../../lib/api";
// Phase deliverables configuration (mirrors backend phase_guidance.rs)
// IDs must match backend phase_guidance.rs exactly for mark_deliverable_complete
interface PhaseDeliverable {
id: string; // Must match backend deliverable ID
name: string;
priority: "required" | "recommended" | "optional";
description: string;
}
interface PhaseConfig {
deliverables: PhaseDeliverable[];
requiresRepository: boolean;
requiresTasks: boolean;
guidance: string;
}
// Contract type specific deliverables (must match backend phase_guidance.rs)
type ContractTypeDeliverables = Partial<Record<ContractPhase, PhaseConfig>>;
const CONTRACT_TYPE_DELIVERABLES: Record<ContractType, ContractTypeDeliverables> = {
simple: {
plan: {
deliverables: [
{ id: "plan-document", name: "Plan", priority: "required", description: "Implementation plan detailing the approach and tasks" },
],
requiresRepository: true,
requiresTasks: false,
guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.",
},
execute: {
deliverables: [
{ id: "pull-request", name: "Pull Request", priority: "required", description: "Pull request with the implemented changes" },
],
requiresRepository: true,
requiresTasks: true,
guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks to finish the contract.",
},
},
specification: {
research: {
deliverables: [
{ id: "research-notes", name: "Research Notes", priority: "required", description: "Document findings and insights during research" },
],
requiresRepository: false,
requiresTasks: false,
guidance: "Focus on understanding the problem space and document your findings in the Research Notes before moving to Specify phase.",
},
specify: {
deliverables: [
{ id: "requirements-document", name: "Requirements Document", priority: "required", description: "Define functional and non-functional requirements" },
],
requiresRepository: false,
requiresTasks: false,
guidance: "Define what needs to be built with clear requirements in the Requirements Document. Ensure specifications are detailed enough for planning.",
},
plan: {
deliverables: [
{ id: "plan-document", name: "Plan", priority: "required", description: "Implementation plan detailing the approach and tasks" },
],
requiresRepository: true,
requiresTasks: false,
guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.",
},
execute: {
deliverables: [
{ id: "pull-request", name: "Pull Request", priority: "required", description: "Pull request with the implemented changes" },
],
requiresRepository: true,
requiresTasks: true,
guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks before moving to Review phase.",
},
review: {
deliverables: [
{ id: "release-notes", name: "Release Notes", priority: "required", description: "Document changes for release communication" },
],
requiresRepository: false,
requiresTasks: false,
guidance: "Review completed work and document the release in the Release Notes. The contract can be completed after review.",
},
},
execute: {
execute: {
deliverables: [], // No deliverables for execute-only contract type
requiresRepository: true,
requiresTasks: true,
guidance: "Execute the tasks directly. No deliverable documents are required for this contract type.",
},
},
};
// Get phase config for a specific contract type and phase
function getPhaseConfig(contractType: ContractType, phase: ContractPhase): PhaseConfig {
const typeConfig = CONTRACT_TYPE_DELIVERABLES[contractType];
const phaseConfig = typeConfig?.[phase];
if (phaseConfig) {
return phaseConfig;
}
// Fallback for unknown phase/type combinations
return {
deliverables: [],
requiresRepository: false,
requiresTasks: false,
guidance: `Unknown phase "${phase}" for contract type "${contractType}"`,
};
}
interface DeliverableStatus {
id: 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) {
// Get phase config based on contract type AND phase
const phaseConfig = useMemo(
() => getPhaseConfig(contract.contractType, contract.phase),
[contract.contractType, contract.phase]
);
// Calculate deliverable status
const deliverableStatuses = useMemo((): DeliverableStatus[] => {
return phaseConfig.deliverables.map((deliverable) => {
// Find matching file by name similarity
const matchedFile = contract.files.find((f) => {
const nameLower = f.name.toLowerCase();
const deliverableLower = deliverable.name.toLowerCase();
return (
f.contractPhase === contract.phase &&
(nameLower.includes(deliverableLower) || deliverableLower.includes(nameLower) || nameLower.includes(deliverable.id.replace("-", " ")))
);
});
return {
...deliverable,
completed: !!matchedFile,
fileId: matchedFile?.id,
actualName: matchedFile?.name,
};
});
}, [contract.files, contract.phase, phaseConfig.deliverables]);
// 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 deliverables
deliverableStatuses.forEach((s) => {
if (s.priority !== "optional") {
total++;
if (s.completed) completed++;
}
});
// Count repository if required
if (phaseConfig.requiresRepository) {
total++;
if (hasRepository) completed++;
}
// Count tasks if required
if (phaseConfig.requiresTasks && taskStats.total > 0) {
total++;
if (taskStats.done === taskStats.total) completed++;
}
return total > 0 ? Math.round((completed / total) * 100) : 100;
}, [deliverableStatuses, hasRepository, phaseConfig, 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">{phaseConfig.guidance}</p>
{/* Deliverables checklist */}
<div className="space-y-2">
{deliverableStatuses.map((status) => (
<div
key={status.id}
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.id, 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 */}
{phaseConfig.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 */}
{phaseConfig.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>
);
}