diff options
| author | soryu <soryu@soryu.co> | 2026-01-11 05:52:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 00:21:16 +0000 |
| commit | 87044a747b47bd83249d61a45842c7f7b2eae56d (patch) | |
| tree | ef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx | |
| parent | 077820c4167c168072d217a1b01df840463a12a8 (diff) | |
| download | soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip | |
Contract system
Diffstat (limited to 'makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx')
| -rw-r--r-- | makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx b/makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx new file mode 100644 index 0000000..da5025b --- /dev/null +++ b/makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx @@ -0,0 +1,301 @@ +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> + ); +} |
