import { useState, useEffect } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useCompany } from "../context/CompanyContext"; import { useDialogActions } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useCloudInstance } from "../hooks/useCloudInstance"; import { companiesApi } from "../api/companies"; import { queryKeys } from "../lib/queryKeys"; import { formatCents, relativeTime } from "../lib/utils"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Badge } from "@/components/ui/badge"; import { Pencil, Check, X, Plus, MoreHorizontal, Trash2, Users, CircleDot, DollarSign, Calendar, ArchiveRestore, } from "lucide-react"; export function Companies() { const { companies, selectedCompanyId, setSelectedCompanyId, loading, error, } = useCompany(); const { openOnboarding } = useDialogActions(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); // A cloud stack holds exactly one company; creating another is a 403 floor // server-side, so the wizard entry point is hidden rather than dead-ending. const isCloud = Boolean(useCloudInstance()); const { data: stats } = useQuery({ queryKey: queryKeys.companies.stats, queryFn: () => companiesApi.stats(), }); // Inline edit state const [editingId, setEditingId] = useState(null); const [editName, setEditName] = useState(""); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const editMutation = useMutation({ mutationFn: ({ id, newName }: { id: string; newName: string }) => companiesApi.update(id, { name: newName }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); setEditingId(null); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => companiesApi.remove(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); queryClient.invalidateQueries({ queryKey: queryKeys.companies.stats }); setConfirmDeleteId(null); }, }); // Unarchiving previously had no UI at all: archiving happens in company // settings, but an archived company disappears from the sidebar switcher, // so its settings page — and with it any way back — was only reachable by // hand-typed URL. This list is the one place that still shows archived // companies, so restoration lives here. const unarchiveMutation = useMutation({ mutationFn: (id: string) => companiesApi.update(id, { status: "active" }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); queryClient.invalidateQueries({ queryKey: queryKeys.companies.stats }); }, }); useEffect(() => { setBreadcrumbs([{ label: "Organizations" }]); }, [setBreadcrumbs]); function startEdit(companyId: string, currentName: string) { setEditingId(companyId); setEditName(currentName); } function saveEdit() { if (!editingId || !editName.trim()) return; editMutation.mutate({ id: editingId, newName: editName.trim() }); } function cancelEdit() { setEditingId(null); setEditName(""); } return (
{isCloud ? null : ( )}
{loading &&

Loading organizations...

} {error &&

{error.message}

}
{companies.map((company) => { const selected = company.id === selectedCompanyId; const isEditing = editingId === company.id; const isConfirmingDelete = confirmDeleteId === company.id; const companyStats = stats?.[company.id]; const agentCount = companyStats?.agentCount ?? 0; const issueCount = companyStats?.issueCount ?? 0; const budgetPct = company.budgetMonthlyCents > 0 ? Math.round( (company.spentMonthlyCents / company.budgetMonthlyCents) * 100, ) : 0; return ( setSelectedCompanyId(company.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setSelectedCompanyId(company.id); } }} interactive className={`block group text-left p-5 ${ selected ? "border-primary ring-1 ring-primary hover:border-primary" : "" }`} > {/* Header row: name + menu */}
{isEditing ? (
e.stopPropagation()} > setEditName(e.target.value)} className="h-7 text-sm" autoFocus onKeyDown={(e) => { if (e.key === "Enter") saveEdit(); if (e.key === "Escape") cancelEdit(); }} />
) : (

{company.name}

{company.status}
)} {company.description && !isEditing && (

{company.description}

)}
{/* Three-dot menu */}
e.stopPropagation()}> startEdit(company.id, company.name)} > Rename {company.status === "archived" && ( unarchiveMutation.mutate(company.id)} > Unarchive )} setConfirmDeleteId(company.id)} > Delete Organization
{/* Stats row */}
{agentCount} {agentCount === 1 ? "agent" : "agents"}
{issueCount} {issueCount === 1 ? "task" : "tasks"}
{formatCents(company.spentMonthlyCents)} {company.budgetMonthlyCents > 0 ? <> / {formatCents(company.budgetMonthlyCents)} ({budgetPct}%) : Unlimited budget}
Created {relativeTime(company.createdAt)}
{/* Delete confirmation */} {isConfirmingDelete && (
e.stopPropagation()} >

Delete this organization and all its data? This cannot be undone.

)}
); })}
); }