import { ChangeEvent, useEffect, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { companiesApi } from "../api/companies"; import { accessApi } from "../api/access"; import { assetsApi } from "../api/assets"; import { queryKeys } from "../lib/queryKeys"; import { Button } from "@/components/ui/button"; import { Settings, Check } from "lucide-react"; import { CompanyPatternIcon } from "../components/CompanyPatternIcon"; import { Field, ToggleField, HintIcon } from "../components/agent-config-primitives"; type AgentSnippetInput = { onboardingTextUrl: string; connectionCandidates?: string[] | null; testResolutionUrl?: string | null; }; export function CompanySettings() { const { companies, selectedCompany, selectedCompanyId, setSelectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); // General settings local state const [companyName, setCompanyName] = useState(""); const [description, setDescription] = useState(""); const [brandColor, setBrandColor] = useState(""); const [logoUrl, setLogoUrl] = useState(""); const [logoUploadError, setLogoUploadError] = useState(null); // Sync local state from selected company useEffect(() => { if (!selectedCompany) return; setCompanyName(selectedCompany.name); setDescription(selectedCompany.description ?? ""); setBrandColor(selectedCompany.brandColor ?? ""); setLogoUrl(selectedCompany.logoUrl ?? ""); }, [selectedCompany]); const [inviteError, setInviteError] = useState(null); const [inviteSnippet, setInviteSnippet] = useState(null); const [snippetCopied, setSnippetCopied] = useState(false); const [snippetCopyDelightId, setSnippetCopyDelightId] = useState(0); const generalDirty = !!selectedCompany && (companyName !== selectedCompany.name || description !== (selectedCompany.description ?? "") || brandColor !== (selectedCompany.brandColor ?? "")); const generalMutation = useMutation({ mutationFn: (data: { name: string; description: string | null; brandColor: string | null; }) => companiesApi.update(selectedCompanyId!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); } }); const settingsMutation = useMutation({ mutationFn: (requireApproval: boolean) => companiesApi.update(selectedCompanyId!, { requireBoardApprovalForNewAgents: requireApproval }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); } }); const inviteMutation = useMutation({ mutationFn: () => accessApi.createCompanyInvite(selectedCompanyId!, { allowedJoinTypes: "agent" }), onSuccess: async (invite) => { setInviteError(null); const base = window.location.origin.replace(/\/+$/, ""); const onboardingTextLink = invite.onboardingTextUrl ?? invite.onboardingTextPath ?? `/api/invites/${invite.token}/onboarding.txt`; const absoluteUrl = onboardingTextLink.startsWith("http") ? onboardingTextLink : `${base}${onboardingTextLink}`; setSnippetCopied(false); setSnippetCopyDelightId(0); let snippet: string; try { const manifest = await accessApi.getInviteOnboarding(invite.token); snippet = buildAgentSnippet({ onboardingTextUrl: absoluteUrl, connectionCandidates: manifest.onboarding.connectivity?.connectionCandidates ?? null, testResolutionUrl: manifest.onboarding.connectivity?.testResolutionEndpoint?.url ?? null }); } catch { snippet = buildAgentSnippet({ onboardingTextUrl: absoluteUrl, connectionCandidates: null, testResolutionUrl: null }); } setInviteSnippet(snippet); try { await navigator.clipboard.writeText(snippet); setSnippetCopied(true); setSnippetCopyDelightId((prev) => prev + 1); setTimeout(() => setSnippetCopied(false), 2000); } catch { /* clipboard may not be available */ } queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId!) }); }, onError: (err) => { setInviteError( err instanceof Error ? err.message : "Failed to create invite" ); } }); const syncLogoState = (nextLogoUrl: string | null) => { setLogoUrl(nextLogoUrl ?? ""); void queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); }; const logoUploadMutation = useMutation({ mutationFn: (file: File) => assetsApi .uploadImage(selectedCompanyId!, file, "companies") .then((asset) => companiesApi.update(selectedCompanyId!, { logoUrl: asset.contentPath })), onSuccess: (company) => { syncLogoState(company.logoUrl); setLogoUploadError(null); } }); const clearLogoMutation = useMutation({ mutationFn: () => companiesApi.update(selectedCompanyId!, { logoUrl: null }), onSuccess: (company) => { setLogoUploadError(null); syncLogoState(company.logoUrl); } }); function handleLogoFileChange(event: ChangeEvent) { const file = event.target.files?.[0] ?? null; event.currentTarget.value = ""; if (!file) return; if (file.size >= 100 * 1024) { setLogoUploadError("Logo image must be smaller than 100 KB."); return; } setLogoUploadError(null); logoUploadMutation.mutate(file); } function handleClearLogo() { clearLogoMutation.mutate(); } useEffect(() => { setInviteError(null); setInviteSnippet(null); setSnippetCopied(false); setSnippetCopyDelightId(0); }, [selectedCompanyId]); const archiveMutation = useMutation({ mutationFn: ({ companyId, nextCompanyId }: { companyId: string; nextCompanyId: string | null; }) => companiesApi.archive(companyId).then(() => ({ nextCompanyId })), onSuccess: async ({ nextCompanyId }) => { if (nextCompanyId) { setSelectedCompanyId(nextCompanyId); } await queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); await queryClient.invalidateQueries({ queryKey: queryKeys.companies.stats }); } }); useEffect(() => { setBreadcrumbs([ { label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: "Settings" } ]); }, [setBreadcrumbs, selectedCompany?.name]); if (!selectedCompany) { return (
No company selected. Select a company from the switcher above.
); } function handleSaveGeneral() { generalMutation.mutate({ name: companyName.trim(), description: description.trim() || null, brandColor: brandColor || null }); } return (

Company Settings

{/* General */}
General
setCompanyName(e.target.value)} /> setDescription(e.target.value)} />
{/* Appearance */}
Appearance
{logoUrl && (
)} {(logoUploadMutation.isError || logoUploadError) && ( {logoUploadError ?? (logoUploadMutation.error instanceof Error ? logoUploadMutation.error.message : "Logo upload failed")} )} {clearLogoMutation.isError && ( {clearLogoMutation.error.message} )} {logoUploadMutation.isPending && ( Uploading logo... )}
setBrandColor(e.target.value)} className="h-8 w-8 cursor-pointer rounded border border-border bg-transparent p-0" /> { const v = e.target.value; if (v === "" || /^#[0-9a-fA-F]{0,6}$/.test(v)) { setBrandColor(v); } }} placeholder="Auto" className="w-28 rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm font-mono outline-none" /> {brandColor && ( )}
{/* Save button for General + Appearance */} {generalDirty && (
{generalMutation.isSuccess && ( Saved )} {generalMutation.isError && ( {generalMutation.error instanceof Error ? generalMutation.error.message : "Failed to save"} )}
)} {/* Hiring */}
Hiring
settingsMutation.mutate(v)} />
{/* Invites */}
Invites
Generate an agent snippet for join flows.
{inviteError && (

{inviteError}

)} {inviteSnippet && (
Agent Snippet
{snippetCopied && ( Copied )}