summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx
blob: b2c2e585f6c7d43c52f23f1a85c1a821bae0197b (plain) (tree)
1
2
3
4
5
6
7
                                
                                                                                        

                                                                       


                                                                                 




                                                    

                                   




                              




















                                                                                                                                                    
    








































                                                                                                                                                               

            





                                                                                                            
    













                                                                                        

                              


                                                                             

                             
             













                                                                                            




                                                                

                                 

                                                                  


                                                      
                                                                

                                               
                                                                                                                                                



              
                       




                                      
                                                                 


















                                                                                                                    

                                                  






                                      
                                         



                                     

                                                           




                                                                   
                                                                   


























                                                                                         
                                                                                    
 
                                    
                                 
                                              
              
                           































                                                                                                          
                                                                    









                                                                                                                                                          
                                          


























                                                                                 

                                     




































                                                                                         
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>
  );
}