summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/contracts/ContractList.tsx
blob: 1eee6a3e7a6f670752b8bb6722ca0411d2c5cc16 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12



                                                                     
                                                            






                                 




                                                         













                                                      




                   

                                                                      












                                                                                                        














                                                                          
                                                        



































                                                                                                                                                        
                                                      













                                                                    
                                                                     









                                                                             









                                                                                                                                                        
                       
                                                                           













                                                                                 
                                                                                                                
















                                                                                             














                                                                          


































                                                                                                                                   
import { useState } from "react";
import type { ContractSummary, ContractStatus } from "../../lib/api";
import { PhaseBadge } from "./PhaseBadge";
import { PhaseProgressBarCompact } from "./PhaseProgressBar";
import { ContractContextMenu } from "./ContractContextMenu";

interface ContractListProps {
  contracts: ContractSummary[];
  loading: boolean;
  onSelect: (id: string) => void;
  onCreate: () => void;
  selectedId?: string;
  onMarkComplete?: (contract: ContractSummary) => void;
  onMarkActive?: (contract: ContractSummary) => void;
  onArchive?: (contract: ContractSummary) => void;
  onDelete?: (contract: ContractSummary) => void;
  onGoToSupervisor?: (contract: ContractSummary) => void;
}

const statusColors: Record<ContractStatus, string> = {
  active: "text-green-400",
  completed: "text-blue-400",
  archived: "text-[#555]",
};

export function ContractList({
  contracts,
  loading,
  onSelect,
  onCreate,
  selectedId,
  onMarkComplete,
  onMarkActive,
  onArchive,
  onDelete,
  onGoToSupervisor,
}: ContractListProps) {
  const [filter, setFilter] = useState<ContractStatus | "all">("all");
  const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
  const [contextMenuContract, setContextMenuContract] = useState<ContractSummary | null>(null);

  const handleContextMenu = (e: React.MouseEvent, contract: ContractSummary) => {
    e.preventDefault();
    setContextMenuPosition({ x: e.clientX, y: e.clientY });
    setContextMenuContract(contract);
  };

  const closeContextMenu = () => {
    setContextMenuPosition(null);
    setContextMenuContract(null);
  };

  const filteredContracts =
    filter === "all"
      ? contracts
      : contracts.filter((c) => c.status === filter);

  if (loading) {
    return (
      <div className="panel h-full flex items-center justify-center">
        <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
      </div>
    );
  }

  return (
    <div className="panel h-full flex flex-col min-h-0">
      {/* Header */}
      <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
        <div className="flex items-center justify-between mb-3">
          <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider">
            Contracts
          </h2>
          <button
            onClick={onCreate}
            className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
          >
            + New
          </button>
        </div>

        {/* Filter tabs */}
        <div className="flex gap-1">
          {(["all", "active", "completed", "archived"] as const).map((status) => (
            <button
              key={status}
              onClick={() => setFilter(status)}
              className={`
                px-2 py-1 font-mono text-[10px] uppercase tracking-wider transition-colors
                ${
                  filter === status
                    ? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]"
                    : "text-[#555] hover:text-[#75aafc]"
                }
              `}
            >
              {status}
            </button>
          ))}
        </div>
      </div>

      {/* Contract list */}
      <div className="flex-1 min-h-0 overflow-y-auto">
        {filteredContracts.length === 0 ? (
          <div className="p-4 text-center">
            <p className="font-mono text-sm text-[#555]">
              {filter === "all"
                ? "No contracts yet"
                : `No ${filter} contracts`}
            </p>
          </div>
        ) : (
          <div className="divide-y divide-[rgba(117,170,252,0.15)]">
            {filteredContracts.map((contract) => (
              <button
                key={contract.id}
                onClick={() => onSelect(contract.id)}
                onContextMenu={(e) => handleContextMenu(e, contract)}
                className={`
                  w-full text-left p-4 transition-colors
                  ${
                    selectedId === contract.id
                      ? "bg-[rgba(117,170,252,0.1)]"
                      : "hover:bg-[rgba(117,170,252,0.05)]"
                  }
                `}
              >
                <div className="flex items-start justify-between gap-2 mb-2">
                  <div className="flex items-center gap-2 min-w-0">
                    <h3 className="font-mono text-sm text-[#dbe7ff] truncate">
                      {contract.name}
                    </h3>
                    {contract.localOnly && (
                      <span className="px-1.5 py-0.5 font-mono text-[9px] uppercase text-amber-400 border border-amber-400/30 bg-amber-400/10 shrink-0">
                        Local
                      </span>
                    )}
                  </div>
                  <span
                    className={`text-[10px] font-mono uppercase shrink-0 ${
                      statusColors[contract.status]
                    }`}
                  >
                    {contract.status}
                  </span>
                </div>

                {contract.description && (
                  <p className="font-mono text-xs text-[#555] mb-2 line-clamp-2">
                    {contract.description}
                  </p>
                )}

                <div className="flex items-center justify-between">
                  <PhaseProgressBarCompact currentPhase={contract.phase} contractType={contract.contractType} />
                  <div className="flex items-center gap-3 text-[10px] font-mono text-[#555]">
                    {contract.fileCount > 0 && (
                      <span>{contract.fileCount} files</span>
                    )}
                    {contract.taskCount > 0 && (
                      <span>{contract.taskCount} tasks</span>
                    )}
                    {contract.repositoryCount > 0 && (
                      <span>{contract.repositoryCount} repos</span>
                    )}
                  </div>
                </div>
              </button>
            ))}
          </div>
        )}
      </div>

      {/* Context Menu */}
      {contextMenuPosition && contextMenuContract && (
        <ContractContextMenu
          x={contextMenuPosition.x}
          y={contextMenuPosition.y}
          contract={contextMenuContract}
          onClose={closeContextMenu}
          onMarkComplete={() => onMarkComplete?.(contextMenuContract)}
          onMarkActive={() => onMarkActive?.(contextMenuContract)}
          onArchive={() => onArchive?.(contextMenuContract)}
          onDelete={() => onDelete?.(contextMenuContract)}
          onGoToSupervisor={() => onGoToSupervisor?.(contextMenuContract)}
        />
      )}
    </div>
  );
}

export function ContractCard({
  contract,
  onClick,
}: {
  contract: ContractSummary;
  onClick: () => void;
}) {
  return (
    <button
      onClick={onClick}
      className="w-full text-left p-4 border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] transition-colors"
    >
      <div className="flex items-start justify-between gap-2 mb-2">
        <h3 className="font-mono text-sm text-[#dbe7ff]">{contract.name}</h3>
        <PhaseBadge phase={contract.phase} />
      </div>

      {contract.description && (
        <p className="font-mono text-xs text-[#555] mb-3 line-clamp-2">
          {contract.description}
        </p>
      )}

      <div className="flex items-center gap-4 text-[10px] font-mono text-[#555]">
        <span>{contract.fileCount} files</span>
        <span>{contract.taskCount} tasks</span>
        <span>{contract.repositoryCount} repos</span>
      </div>
    </button>
  );
}