"use client"; import { Building2Icon, CoinsIcon, CreditCardIcon, PlugZapIcon, ShieldCheckIcon, UsersIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { getStripePublishableKey } from "../../../client/config"; import { MicroLabel, RoundPageContainer } from "../../../components"; import { Link } from "../../../shadcnui"; import { AdministrationProvider } from "../contexts/AdministrationContext"; import type { AdminGroup, AdminSection } from "../data/admin-section.types"; /** * The sections every NJA project has. Hrefs are constants rather than props: * the path convention IS the contract that makes this component reusable, and * it already exists in the package (see TOKEN_USAGE_ADMIN_PAGE_URL in * features/tokenusage/contexts/TokenUsageAdminContext.tsx). */ const PLATFORM_SECTIONS: AdminSection[] = [ { key: "companies", icon: Building2Icon, labelKey: "entities.companies", labelValues: { count: 2 }, descriptionKey: "administration.companies.description", href: "/administration/companies", }, { key: "users", icon: UsersIcon, labelKey: "entities.users", labelValues: { count: 2 }, descriptionKey: "administration.users.description", href: "/administration/users", }, { key: "token-usage", icon: CoinsIcon, labelKey: "token_usage.admin.title", descriptionKey: "administration.token_usage.description", href: "/administration/token-usage", }, { key: "ai-connections", icon: PlugZapIcon, labelKey: "ai_connections.admin.title", descriptionKey: "administration.ai_connections.description", href: "/administration/ai-connections", }, { key: "rbac", icon: ShieldCheckIcon, labelKey: "entities.rbac", labelValues: { count: 2 }, descriptionKey: "administration.rbac.description", href: "/administration/rbac", }, ]; /** * The only conditional entry. getStripePublishableKey comes from /client rather * than isStripeConfigured() from /billing on purpose: /billing exists to keep * Stripe.js out of the global bundle, and isStripeConfigured lives in * StripeProvider.tsx, which imports @stripe/stripe-js at module scope. The two * are the same check — isStripeConfigured is a one-line wrapper over this call. */ const BILLING_SECTION: AdminSection = { key: "billing", icon: CreditCardIcon, labelKey: "billing.admin.products.title", descriptionKey: "administration.billing.description", href: "/administration/products", }; /** Keys of the built-in platform sections, for `hiddenSections`. */ export type PlatformSectionKey = "companies" | "users" | "token-usage" | "ai-connections" | "rbac" | "billing"; type AdminIndexProps = { /** Project-specific groups, appended after the built-in platform group. */ additionalGroups?: AdminGroup[]; /** * Built-in sections to leave out. Every NJA project gets the same platform * group by default, but a project whose backend does not wire a foundation * must be able to drop its entry rather than advertise a route that throws — * narr8, for instance, never calls `RbacModule.register()`, so it has no RBAC * controllers at all. */ hiddenSections?: PlatformSectionKey[]; }; /** * The grid itself, with no page shell — this is what the specs render, because * RoundPageContainer pulls in the app Header and its four header contexts. */ export function AdminIndexGrid({ additionalGroups = [], hiddenSections = [] }: AdminIndexProps) { const t = useTranslations(); const available = getStripePublishableKey() ? [...PLATFORM_SECTIONS, BILLING_SECTION] : PLATFORM_SECTIONS; const platformGroup: AdminGroup = { key: "platform", labelKey: "administration.group.platform", sections: available.filter((section) => !hiddenSections.includes(section.key as PlatformSectionKey)), }; const groups = [platformGroup, ...additionalGroups].filter((group) => group.sections.length > 0); return (
{t("administration.subtitle")}
{groups.map((group) => (