import { useState, useCallback, useEffect } from "react"; import { useParams, useNavigate } from "react-router"; import { Masthead } from "../components/Masthead"; import { ContractList } from "../components/contracts/ContractList"; import { ContractDetail } from "../components/contracts/ContractDetail"; import { DirectoryInput } from "../components/mesh/DirectoryInput"; import { useContracts } from "../hooks/useContracts"; import { useAuth } from "../contexts/AuthContext"; import { createTask, getDaemonDirectories, getRepositorySuggestions } from "../lib/api"; import type { ContractWithRelations, ContractSummary, ContractPhase, ContractStatus, ContractType, CreateContractRequest, RepositorySourceType, DaemonDirectory, RepositoryHistoryEntry, } from "../lib/api"; import { getValidPhases, getDefaultPhase } from "../lib/api"; export default function ContractsPage() { const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); const navigate = useNavigate(); // Redirect to login if not authenticated (when auth is configured) useEffect(() => { if (!authLoading && isAuthConfigured && !isAuthenticated) { navigate("/login"); } }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); // Show loading while checking auth if (authLoading) { return (

Loading...

); } // Don't render if not authenticated (will redirect) if (isAuthConfigured && !isAuthenticated) { return null; } return ; } function ContractsPageContent() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { contracts, loading, error, fetchContract, saveContract, editContract, removeContract, changePhase, addRemoteRepo, addLocalRepo, createManagedRepo, removeRepo, setRepoPrimary, } = useContracts(); const [contractDetail, setContractDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [isCreating, setIsCreating] = useState(false); const [newContractName, setNewContractName] = useState(""); const [newContractDescription, setNewContractDescription] = useState(""); const [contractType, setContractType] = useState("simple"); const [initialPhase, setInitialPhase] = useState("plan"); const [repoType, setRepoType] = useState("remote"); const [repoName, setRepoName] = useState(""); const [repoUrl, setRepoUrl] = useState(""); const [repoPath, setRepoPath] = useState(""); const [createError, setCreateError] = useState(null); const [suggestedDirectories, setSuggestedDirectories] = useState([]); const [repoSuggestions, setRepoSuggestions] = useState([]); const [showRepoSuggestions, setShowRepoSuggestions] = useState(false); // Fetch repository suggestions when modal opens and repo type changes useEffect(() => { if (isCreating && (repoType === "remote" || repoType === "local")) { getRepositorySuggestions(repoType, undefined, 10) .then((res) => { setRepoSuggestions(res.entries); setShowRepoSuggestions(res.entries.length > 0); }) .catch(() => { setRepoSuggestions([]); setShowRepoSuggestions(false); }); } else { setRepoSuggestions([]); setShowRepoSuggestions(false); } }, [isCreating, repoType]); // Apply a repository suggestion const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => { setRepoName(suggestion.name); if (suggestion.repositoryUrl) { setRepoUrl(suggestion.repositoryUrl); } if (suggestion.localPath) { setRepoPath(suggestion.localPath); } setShowRepoSuggestions(false); }, []); // Fetch daemon directories when "local" repo type is selected useEffect(() => { if (repoType === "local" && isCreating) { getDaemonDirectories() .then((res) => setSuggestedDirectories(res.directories)) .catch(() => setSuggestedDirectories([])); } }, [repoType, isCreating]); // Load contract detail when ID changes useEffect(() => { if (id) { setDetailLoading(true); fetchContract(id).then((contract) => { setContractDetail(contract); setDetailLoading(false); }); } else { setContractDetail(null); } }, [id, fetchContract]); const handleSelect = useCallback( (contractId: string) => { navigate(`/contracts/${contractId}`); }, [navigate] ); const handleBack = useCallback(() => { navigate("/contracts"); }, [navigate]); const handleCreate = useCallback(() => { setIsCreating(true); }, []); // Validate repository configuration const isRepoValid = useCallback(() => { if (!repoName.trim()) return false; if (repoType === "remote" && !repoUrl.trim()) return false; if (repoType === "local" && !repoPath.trim()) return false; return true; }, [repoType, repoName, repoUrl, repoPath]); const handleCreateSubmit = useCallback(async () => { if (!newContractName.trim()) return; if (!isRepoValid()) { setCreateError("Repository configuration is required"); return; } setCreateError(null); const data: CreateContractRequest = { name: newContractName.trim(), description: newContractDescription.trim() || undefined, contractType: contractType, initialPhase: initialPhase !== getDefaultPhase(contractType) ? initialPhase : undefined, }; try { const contract = await saveContract(data); if (contract) { // Add the repository after contract creation try { if (repoType === "remote") { await addRemoteRepo(contract.id, { name: repoName.trim(), repositoryUrl: repoUrl.trim(), isPrimary: true, }); } else if (repoType === "local") { await addLocalRepo(contract.id, { name: repoName.trim(), localPath: repoPath.trim(), isPrimary: true, }); } else if (repoType === "managed") { await createManagedRepo(contract.id, { name: repoName.trim(), isPrimary: true, }); } } catch (repoError) { console.error("Failed to add repository:", repoError); // Still navigate to the contract - repo can be added later } // Clear form state setIsCreating(false); setNewContractName(""); setNewContractDescription(""); setContractType("simple"); setInitialPhase("plan"); setRepoType("remote"); setRepoName(""); setRepoUrl(""); setRepoPath(""); navigate(`/contracts/${contract.id}`); } } catch (err) { setCreateError(err instanceof Error ? err.message : "Failed to create contract"); } }, [ newContractName, newContractDescription, contractType, initialPhase, repoType, repoName, repoUrl, repoPath, isRepoValid, saveContract, addRemoteRepo, addLocalRepo, createManagedRepo, navigate, ]); const handleCreateCancel = useCallback(() => { setIsCreating(false); setNewContractName(""); setNewContractDescription(""); setContractType("simple"); setInitialPhase("plan"); setRepoType("remote"); setRepoName(""); setRepoUrl(""); setRepoPath(""); setCreateError(null); }, []); const handleUpdate = useCallback( async (name: string, description: string) => { if (contractDetail) { const updated = await editContract(contractDetail.id, { name, description: description || undefined, version: contractDetail.version, }); if (updated) { // Refresh detail const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } } }, [contractDetail, editContract, fetchContract] ); const handleDelete = useCallback(async () => { if (contractDetail && confirm("Are you sure you want to delete this contract?")) { const success = await removeContract(contractDetail.id); if (success) { navigate("/contracts"); } } }, [contractDetail, removeContract, navigate]); const handlePhaseChange = useCallback( async (phase: ContractPhase) => { if (contractDetail) { const updated = await changePhase(contractDetail.id, phase); if (updated) { // Refresh detail const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } } }, [contractDetail, changePhase, fetchContract] ); const handleStatusChange = useCallback( async (status: ContractStatus) => { if (contractDetail) { const updated = await editContract(contractDetail.id, { status, version: contractDetail.version, }); if (updated) { // Refresh detail const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } } }, [contractDetail, editContract, fetchContract] ); // Repository handlers const handleAddRemoteRepo = useCallback( async (name: string, url: string, isPrimary: boolean) => { if (contractDetail) { await addRemoteRepo(contractDetail.id, { name, repositoryUrl: url, isPrimary }); const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } }, [contractDetail, addRemoteRepo, fetchContract] ); const handleAddLocalRepo = useCallback( async (name: string, path: string, isPrimary: boolean) => { if (contractDetail) { await addLocalRepo(contractDetail.id, { name, localPath: path, isPrimary }); const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } }, [contractDetail, addLocalRepo, fetchContract] ); const handleCreateManagedRepo = useCallback( async (name: string, isPrimary: boolean) => { if (contractDetail) { await createManagedRepo(contractDetail.id, { name, isPrimary }); const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } }, [contractDetail, createManagedRepo, fetchContract] ); const handleDeleteRepo = useCallback( async (repoId: string) => { if (contractDetail) { await removeRepo(contractDetail.id, repoId); const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } }, [contractDetail, removeRepo, fetchContract] ); const handleSetRepoPrimary = useCallback( async (repoId: string) => { if (contractDetail) { await setRepoPrimary(contractDetail.id, repoId); const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } }, [contractDetail, setRepoPrimary, fetchContract] ); // Refresh contract detail (used after file/task operations) const handleRefresh = useCallback(async () => { if (contractDetail) { const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); } }, [contractDetail, fetchContract]); // File/task navigation handlers const handleFileSelect = useCallback( (fileId: string) => { navigate(`/files/${fileId}`); }, [navigate] ); const handleTaskSelect = useCallback( (taskId: string) => { navigate(`/mesh/${taskId}`); }, [navigate] ); // Create task within contract context const handleTaskCreate = useCallback( async (name: string, plan: string, repositoryUrl?: string) => { if (!contractDetail) return; try { // Create the task with contract_id (task is automatically associated) const task = await createTask({ contractId: contractDetail.id, name, plan, repositoryUrl, }); // Refresh contract detail to show new task const refreshed = await fetchContract(contractDetail.id); setContractDetail(refreshed); // Navigate to the new task navigate(`/mesh/${task.id}`); } catch (e) { console.error("Failed to create task:", e); alert(e instanceof Error ? e.message : "Failed to create task"); } }, [contractDetail, fetchContract, navigate] ); // Context menu handlers for ContractList const handleContextMarkComplete = useCallback( async (contract: ContractSummary) => { await editContract(contract.id, { status: "completed", version: contract.version }); }, [editContract] ); const handleContextMarkActive = useCallback( async (contract: ContractSummary) => { await editContract(contract.id, { status: "active", version: contract.version }); }, [editContract] ); const handleContextArchive = useCallback( async (contract: ContractSummary) => { await editContract(contract.id, { status: "archived", version: contract.version }); }, [editContract] ); const handleContextDelete = useCallback( async (contract: ContractSummary) => { if (confirm(`Are you sure you want to delete "${contract.name}"?`)) { const success = await removeContract(contract.id); if (success && contract.id === id) { navigate("/contracts"); } } }, [removeContract, id, navigate] ); const handleContextGoToSupervisor = useCallback( (contract: ContractSummary) => { if (contract.supervisorTaskId) { navigate(`/mesh/${contract.supervisorTaskId}`); } }, [navigate] ); return (
{error && (
{error}
)} {/* Create contract modal */} {isCreating && (

Create Contract

{createError && (
{createError}
)}
{/* Contract name */}
setNewContractName(e.target.value)} placeholder="Contract name" className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" autoFocus />
{/* Description */}