summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-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.tsx301
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>
+ );
+}