import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import type { Company } from "@paperclipai/shared"; import { companiesApi } from "../api/companies"; import { companiesListQueryOptions, type CompanyListResult } from "../api/companies-query"; import { queryKeys } from "../lib/queryKeys"; import type { CompanySelectionSource } from "../lib/company-selection"; type CompanySelectionOptions = { source?: CompanySelectionSource }; interface CompanyContextValue { companies: Company[]; selectedCompanyId: string | null; selectedCompany: Company | null; selectionSource: CompanySelectionSource; loading: boolean; error: Error | null; setSelectedCompanyId: (companyId: string, options?: CompanySelectionOptions) => void; reloadCompanies: () => Promise; createCompany: (data: { name: string; description?: string | null; budgetMonthlyCents?: number; }) => Promise; } const STORAGE_KEY = "paperclip.selectedCompanyId"; const CompanyContext = createContext(null); export function resolveBootstrapCompanySelection(input: { companies: Array>; sidebarCompanies: Array>; selectedCompanyId: string | null; storedCompanyId: string | null; }) { if (input.companies.length === 0) return null; const selectableCompanies = input.sidebarCompanies.length > 0 ? input.sidebarCompanies : input.companies; if (input.selectedCompanyId && selectableCompanies.some((company) => company.id === input.selectedCompanyId)) { return input.selectedCompanyId; } if (input.storedCompanyId && selectableCompanies.some((company) => company.id === input.storedCompanyId)) { return input.storedCompanyId; } return selectableCompanies[0]?.id ?? null; } export function shouldClearStoredCompanySelection(input: { companies: Array>; isLoading: boolean; unauthorized: boolean; }) { return !input.isLoading && !input.unauthorized && input.companies.length === 0; } export function CompanyProvider({ children }: { children: ReactNode }) { const queryClient = useQueryClient(); const [selectionSource, setSelectionSource] = useState("bootstrap"); const [selectedCompanyId, setSelectedCompanyIdState] = useState(null); const { data: companiesResult = { companies: [], unauthorized: false }, isLoading, error } = useQuery(companiesListQueryOptions); const companies = companiesResult.companies; const companyListUnauthorized = companiesResult.unauthorized; const sidebarCompanies = useMemo( () => companies.filter((company) => company.status !== "archived"), [companies], ); // Auto-select first company when list loads useEffect(() => { if (isLoading) return; if (companies.length === 0) { if (shouldClearStoredCompanySelection({ companies, isLoading: false, unauthorized: companyListUnauthorized })) { if (selectedCompanyId !== null) { setSelectedCompanyIdState(null); } localStorage.removeItem(STORAGE_KEY); } return; } const next = resolveBootstrapCompanySelection({ companies, sidebarCompanies, selectedCompanyId, storedCompanyId: localStorage.getItem(STORAGE_KEY), }); if (next === null || next === selectedCompanyId) return; setSelectedCompanyIdState(next); setSelectionSource("bootstrap"); localStorage.setItem(STORAGE_KEY, next); }, [companies, companyListUnauthorized, isLoading, selectedCompanyId, sidebarCompanies]); const setSelectedCompanyId = useCallback((companyId: string, options?: CompanySelectionOptions) => { setSelectedCompanyIdState(companyId); setSelectionSource(options?.source ?? "manual"); localStorage.setItem(STORAGE_KEY, companyId); }, []); const reloadCompanies = useCallback(async () => { await queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); }, [queryClient]); const createMutation = useMutation({ mutationFn: (data: { name: string; description?: string | null; budgetMonthlyCents?: number; }) => companiesApi.create(data), onSuccess: (company) => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); setSelectedCompanyId(company.id); }, }); const createCompany = useCallback( async (data: { name: string; description?: string | null; budgetMonthlyCents?: number; }) => { return createMutation.mutateAsync(data); }, [createMutation], ); const selectedCompany = useMemo( () => companies.find((company) => company.id === selectedCompanyId) ?? null, [companies, selectedCompanyId], ); const value = useMemo( () => ({ companies, selectedCompanyId, selectedCompany, selectionSource, loading: isLoading, error: error as Error | null, setSelectedCompanyId, reloadCompanies, createCompany, }), [ companies, selectedCompanyId, selectedCompany, selectionSource, isLoading, error, setSelectedCompanyId, reloadCompanies, createCompany, ], ); return {children}; } export function useCompany() { const ctx = useContext(CompanyContext); if (!ctx) { throw new Error("useCompany must be used within CompanyProvider"); } return ctx; } /** * Non-throwing variant of {@link useCompany}. Returns null when called outside a * CompanyProvider instead of throwing, so components that may render in * provider-less surfaces (e.g. exported/standalone markdown) can read company * state without crashing. */ export function useOptionalCompany(): CompanyContextValue | null { return useContext(CompanyContext); }