Files
paperclip/ui/src/context/CompanyContext.tsx
Forgotten 410164a632 feat(ui): company-prefix routes, archive company, hide archived from sidebar
Support optional company-prefix in URL paths (e.g. /PAP/issues/PAP-1).
Filter archived companies from sidebar rail, switcher, and auto-select.
Add archive button to company settings with confirmation dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:31:54 -06:00

152 lines
4.5 KiB
TypeScript

import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { Company } from "@paperclip/shared";
import { companiesApi } from "../api/companies";
import { ApiError } from "../api/client";
import { queryKeys } from "../lib/queryKeys";
interface CompanyContextValue {
companies: Company[];
selectedCompanyId: string | null;
selectedCompany: Company | null;
loading: boolean;
error: Error | null;
setSelectedCompanyId: (companyId: string) => void;
reloadCompanies: () => Promise<void>;
createCompany: (data: {
name: string;
description?: string | null;
budgetMonthlyCents?: number;
}) => Promise<Company>;
}
const STORAGE_KEY = "paperclip.selectedCompanyId";
const CompanyContext = createContext<CompanyContextValue | null>(null);
export function CompanyProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
const [selectedCompanyId, setSelectedCompanyIdState] = useState<string | null>(
() => {
// Check URL param first (supports "open in new tab" from company rail)
const urlParams = new URLSearchParams(window.location.search);
const companyParam = urlParams.get("company");
if (companyParam) {
localStorage.setItem(STORAGE_KEY, companyParam);
// Clean up the URL param
urlParams.delete("company");
const newSearch = urlParams.toString();
const newUrl =
window.location.pathname + (newSearch ? `?${newSearch}` : "");
window.history.replaceState({}, "", newUrl);
return companyParam;
}
return localStorage.getItem(STORAGE_KEY);
}
);
const { data: companies = [], isLoading, error } = useQuery({
queryKey: queryKeys.companies.all,
queryFn: async () => {
try {
return await companiesApi.list();
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
return [];
}
throw err;
}
},
retry: false,
});
const sidebarCompanies = useMemo(
() => companies.filter((company) => company.status !== "archived"),
[companies],
);
// Auto-select first company when list loads
useEffect(() => {
if (companies.length === 0) return;
const selectableCompanies = sidebarCompanies.length > 0 ? sidebarCompanies : companies;
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && selectableCompanies.some((c) => c.id === stored)) return;
if (selectedCompanyId && selectableCompanies.some((c) => c.id === selectedCompanyId)) return;
const next = selectableCompanies[0]!.id;
setSelectedCompanyIdState(next);
localStorage.setItem(STORAGE_KEY, next);
}, [companies, selectedCompanyId, sidebarCompanies]);
const setSelectedCompanyId = useCallback((companyId: string) => {
setSelectedCompanyIdState(companyId);
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,
loading: isLoading,
error: error as Error | null,
setSelectedCompanyId,
reloadCompanies,
createCompany,
}),
[
companies,
selectedCompanyId,
selectedCompany,
isLoading,
error,
setSelectedCompanyId,
reloadCompanies,
createCompany,
],
);
return <CompanyContext.Provider value={value}>{children}</CompanyContext.Provider>;
}
export function useCompany() {
const ctx = useContext(CompanyContext);
if (!ctx) {
throw new Error("useCompany must be used within CompanyProvider");
}
return ctx;
}