diff options
Diffstat (limited to 'makima/frontend/src/routes/workflow.tsx')
| -rw-r--r-- | makima/frontend/src/routes/workflow.tsx | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/makima/frontend/src/routes/workflow.tsx b/makima/frontend/src/routes/workflow.tsx new file mode 100644 index 0000000..cb72e9e --- /dev/null +++ b/makima/frontend/src/routes/workflow.tsx @@ -0,0 +1,205 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import { useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { WorkflowBoard } from "../components/workflow/WorkflowBoard"; +import { useContracts } from "../hooks/useContracts"; +import { useAuth } from "../contexts/AuthContext"; +import type { ContractPhase, ContractStatus } from "../lib/api"; + +type StatusFilter = "all" | ContractStatus; + +export default function WorkflowPage() { + 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 ( + <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> + <Masthead showNav /> + <main className="flex-1 flex items-center justify-center"> + <p className="text-[#7788aa] font-mono text-sm">Loading...</p> + </main> + </div> + ); + } + + // Don't render if not authenticated (will redirect) + if (isAuthConfigured && !isAuthenticated) { + return null; + } + + return <WorkflowPageContent />; +} + +function WorkflowPageContent() { + const navigate = useNavigate(); + const { contracts, loading, error, changePhase, saveContract } = useContracts(); + const [statusFilter, setStatusFilter] = useState<StatusFilter>("all"); + const [isCreating, setIsCreating] = useState(false); + const [newContractName, setNewContractName] = useState(""); + + // Filter contracts by status + const filteredContracts = useMemo(() => { + if (statusFilter === "all") { + return contracts; + } + return contracts.filter((c) => c.status === statusFilter); + }, [contracts, statusFilter]); + + const handleContractClick = useCallback( + (contractId: string) => { + navigate(`/contracts/${contractId}`); + }, + [navigate] + ); + + const handlePhaseChange = useCallback( + async (contractId: string, newPhase: ContractPhase) => { + await changePhase(contractId, newPhase); + }, + [changePhase] + ); + + const handleCreateContract = useCallback(async () => { + if (!newContractName.trim()) return; + const contract = await saveContract({ + name: newContractName.trim(), + }); + if (contract) { + setNewContractName(""); + setIsCreating(false); + navigate(`/contracts/${contract.id}`); + } + }, [newContractName, saveContract, navigate]); + + const handleCancelCreate = useCallback(() => { + setNewContractName(""); + setIsCreating(false); + }, []); + + return ( + <div className="relative z-10 h-screen flex flex-col bg-[#0a1628]"> + <Masthead showNav /> + <main className="flex-1 flex flex-col p-4 pt-2 gap-4 overflow-hidden"> + {error && ( + <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm shrink-0"> + {error} + </div> + )} + + {/* Header with filter and create button */} + <div className="flex items-center justify-between shrink-0"> + <div className="flex items-center gap-4"> + <h1 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider"> + Board + </h1> + {/* Status filter */} + <div className="flex items-center gap-1"> + {(["all", "active", "completed", "archived"] as StatusFilter[]).map( + (status) => ( + <button + key={status} + onClick={() => setStatusFilter(status)} + className={` + px-2 py-1 font-mono text-[10px] uppercase transition-colors + ${ + statusFilter === status + ? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]" + : "text-[#555] border border-transparent hover:text-[#75aafc]" + } + `} + > + {status} + </button> + ) + )} + </div> + </div> + <button + onClick={() => setIsCreating(true)} + className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors" + > + + New Contract + </button> + </div> + + {/* Create contract modal */} + {isCreating && ( + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> + <div className="w-full max-w-md p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]"> + <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> + Create Contract + </h3> + <div className="space-y-4"> + <input + type="text" + value={newContractName} + onChange={(e) => 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 + onKeyDown={(e) => { + if (e.key === "Enter") handleCreateContract(); + if (e.key === "Escape") handleCancelCreate(); + }} + /> + <div className="flex gap-2 justify-end"> + <button + onClick={handleCancelCreate} + className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleCreateContract} + disabled={!newContractName.trim()} + className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + > + Create + </button> + </div> + </div> + </div> + </div> + )} + + {/* Board */} + <div className="flex-1 min-h-0 overflow-hidden"> + {loading ? ( + <div className="h-full flex items-center justify-center"> + <p className="font-mono text-sm text-[#555]">Loading...</p> + </div> + ) : filteredContracts.length === 0 && statusFilter === "all" ? ( + <div className="h-full flex items-center justify-center"> + <div className="text-center"> + <p className="font-mono text-sm text-[#555] mb-4"> + No contracts yet + </p> + <button + onClick={() => setIsCreating(true)} + className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" + > + + Create First Contract + </button> + </div> + </div> + ) : ( + <WorkflowBoard + contracts={filteredContracts} + onContractClick={handleContractClick} + onPhaseChange={handlePhaseChange} + /> + )} + </div> + </main> + </div> + ); +} |
