summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/workflow.tsx
blob: e12209290d5fc0e49996669d760b0fd233d89f4a (plain) (tree)
1
2
3
4
5
6
7





                                                                     
                                                                                 



































                                                                                  
                                                                                                                

























                                                                        







































                                                                                          































































































































                                                                                                                                                                                                    




                                                            






              
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, ContractSummary } 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, editContract, removeContract } = 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]
  );

  // Context menu handlers
  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}"?`)) {
        await removeContract(contract.id);
      }
    },
    [removeContract]
  );

  const handleContextGoToSupervisor = useCallback(
    (contract: ContractSummary) => {
      if (contract.supervisorTaskId) {
        navigate(`/mesh/${contract.supervisorTaskId}`);
      }
    },
    [navigate]
  );

  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}
              onMarkComplete={handleContextMarkComplete}
              onMarkActive={handleContextMarkActive}
              onArchive={handleContextArchive}
              onDelete={handleContextDelete}
              onGoToSupervisor={handleContextGoToSupervisor}
            />
          )}
        </div>
      </main>
    </div>
  );
}