"use client"; import { useTranslations } from "next-intl"; import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { SharedProvider } from "../../../contexts"; import { usePageUrlGenerator } from "../../../hooks"; import { BreadcrumbItemData } from "../../../interfaces"; import { TokenUsageAdminFilterBar } from "../components/TokenUsageAdminFilterBar"; import type { TokenUsageAdminBreakdownInterface } from "../data/tokenusage-admin-breakdown.interface"; import type { TokenUsageAdminSummaryInterface } from "../data/tokenusage-admin-summary.interface"; import type { TokenUsageAdminTimelineInterface } from "../data/tokenusage-admin-timeline.interface"; import type { Granularity, Metric, StackBy } from "../data/tokenusage-admin.types"; import { TokenUsageAdminService } from "../data/TokenUsageAdminService"; /** * Default route the breadcrumb links back to. A host app that mounts the page * elsewhere overrides it with the `pageUrl` prop — the constant stays here * because the breadcrumb is built by this provider, not by the app. */ const TOKEN_USAGE_ADMIN_PAGE_URL = "/administration/token-usage"; /** Rows kept per ranked panel before the repository folds the tail into "other". */ const DEFAULT_TOP_N = 10; export type TokenUsageAdminFilterState = { /** ISO 8601 instant. */ from: string; /** ISO 8601 instant. */ to: string; granularity: Granularity; stackBy: StackBy; companyId?: string; metric: Metric; }; export interface TokenUsageAdminContextType { summary: TokenUsageAdminSummaryInterface[]; timeline: TokenUsageAdminTimelineInterface[]; byCompany: TokenUsageAdminBreakdownInterface[]; byUser: TokenUsageAdminBreakdownInterface[]; /** Platform-side spend split by operation. Always empty in single-customer mode. */ /** Platform spend by operation — empty in single-customer mode. */ byOperation: TokenUsageAdminBreakdownInterface[]; /** Customer spend by operation — the customer-side mirror of byOperation. */ byCustomerOperation: TokenUsageAdminBreakdownInterface[]; companies: { id: string; label: string }[]; filters: TokenUsageAdminFilterState; /** Merges a partial patch into the current filters; every key is optional. */ setFilters: (next: Partial) => void; /** `true` once a company filter is applied — the platform panels are meaningless then. */ singleCustomerMode: boolean; isLoading: boolean; error: string | null; } const TokenUsageAdminContext = createContext(undefined); /** Current calendar month to now, which is the window the page opens on. */ function defaultRange(): { from: string; to: string } { const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0); return { from: start.toISOString(), to: now.toISOString() }; } type TokenUsageAdminProviderProps = { children: ReactNode; /** Pre-select a company, which puts the page in single-customer mode from the first render. */ initialCompanyId?: string; /** ISO 8601 instant. Defaults to the start of the current month. */ initialFrom?: string; /** ISO 8601 instant. Defaults to now. */ initialTo?: string; /** Rows per ranked panel. Defaults to 10. */ topN?: number; /** Route the breadcrumb links back to. */ pageUrl?: string; }; /** * Owns every filter the administrative token-usage page reads, fetches the five * panels behind it, and publishes the filter bar into the page title bar. * * The filter bar is rendered into `title.functions` here — NOT in the container — * because `RoundPageContainer`'s title bar reads `title.functions` from * `SharedContext`, and a descendant cannot inject nodes into an ancestor's * provider value. That is why the filter state lives at this level. */ export const TokenUsageAdminProvider = ({ children, initialCompanyId, initialFrom, initialTo, topN = DEFAULT_TOP_N, pageUrl = TOKEN_USAGE_ADMIN_PAGE_URL, }: TokenUsageAdminProviderProps) => { const t = useTranslations(); const generateUrl = usePageUrlGenerator(); const [filters, setFilterState] = useState(() => { const range = defaultRange(); return { from: initialFrom ?? range.from, to: initialTo ?? range.to, granularity: "day", stackBy: "scope", companyId: initialCompanyId, metric: "cost", }; }); const [summary, setSummary] = useState([]); const [timeline, setTimeline] = useState([]); const [byCompany, setByCompany] = useState([]); const [byUser, setByUser] = useState([]); const [byOperation, setByOperation] = useState([]); const [byCustomerOperation, setByCustomerOperation] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const { from, to, granularity, stackBy, companyId, metric } = filters; const singleCustomerMode = Boolean(companyId); useEffect(() => { // `cancelled` is the out-of-order guard: a filter change fires a new request // while the previous one is still in flight, and without this flag the slower // (older) response would land last and overwrite the fresher state. let cancelled = false; setIsLoading(true); setError(null); const base = { from, to, companyId }; Promise.all([ TokenUsageAdminService.getSummary(base), TokenUsageAdminService.getTimeline({ ...base, granularity, stackBy }), TokenUsageAdminService.getBreakdown({ ...base, dimension: "company", scope: "customer", limit: topN }), TokenUsageAdminService.getBreakdown({ ...base, dimension: "user", scope: "customer", limit: topN }), // Platform spend is, by definition, the usage with no owning company, so // filtering it by one is meaningless — the call is skipped entirely rather // than issued and discarded. companyId ? Promise.resolve([]) : TokenUsageAdminService.getBreakdown({ ...base, dimension: "operation", scope: "platform", limit: topN }), // Customer spend by operation — unlike the platform panel this one IS // meaningful under a company filter, so it is always requested. TokenUsageAdminService.getBreakdown({ ...base, dimension: "operation", scope: "customer", limit: topN }), ]) .then(([nextSummary, nextTimeline, nextByCompany, nextByUser, nextByOperation, nextByCustomerOperation]) => { if (cancelled) return; setSummary(nextSummary ?? []); setTimeline(nextTimeline ?? []); setByCompany(nextByCompany ?? []); setByUser(nextByUser ?? []); setByOperation(nextByOperation ?? []); setByCustomerOperation(nextByCustomerOperation ?? []); }) .catch((err) => { if (cancelled) return; console.error("Failed to load administrative token usage:", err); setError(err instanceof Error ? err.message : String(err)); }) .finally(() => { if (cancelled) return; setIsLoading(false); }); return () => { cancelled = true; }; }, [from, to, granularity, stackBy, companyId, topN]); const setFilters = useCallback((next: Partial) => { setFilterState((prev) => ({ ...prev, ...next })); }, []); /** * Derived from the company breakdown, per the page design. Note the * consequence: once a company filter is applied the breakdown returns that * company alone, so the selector narrows to it — clearing the filter restores * the full list. */ const companies = useMemo( () => byCompany.filter((row) => row.id !== "other").map((row) => ({ id: row.id, label: row.label })), [byCompany], ); const breadcrumb = (): BreadcrumbItemData[] => [ { name: t("token_usage.admin.title"), href: generateUrl({ page: pageUrl }), }, ]; const title = () => ({ type: t("token_usage.admin.title"), functions: ( ), }); const contextValue = useMemo( () => ({ summary, timeline, byCompany, byUser, byOperation, byCustomerOperation, companies, filters, setFilters, singleCustomerMode, isLoading, error, }), [ summary, timeline, byCompany, byUser, byOperation, byCustomerOperation, companies, filters, setFilters, singleCustomerMode, isLoading, error, ], ); return ( {children} ); }; export const useTokenUsageAdmin = (): TokenUsageAdminContextType => { const ctx = useContext(TokenUsageAdminContext); if (!ctx) { throw new Error("useTokenUsageAdmin() called outside ."); } return ctx; };