import { AccountQuotaPanels } from "./AccountQuotaPanels";
import { quotaUnavailableMessage } from "@/lib/quota-refresh";
import { useMemo } from "react";
import type { CostByProviderModel, CostWindowSpendRow, QuotaWindow, ProviderQuotaResult } from "@paperclipai/shared";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { QuotaBar } from "./QuotaBar";
import { CostAmount } from "./CostAmount";
import { ClaudeSubscriptionPanel } from "./ClaudeSubscriptionPanel";
import { CodexSubscriptionPanel } from "./CodexSubscriptionPanel";
import {
billingTypeDisplayName,
formatCents,
formatTokens,
providerDisplayName,
quotaSourceDisplayName,
} from "@/lib/utils";
// ordered display labels for rolling-window rows
const ROLLING_WINDOWS = ["5h", "24h", "7d"] as const;
interface ProviderQuotaCardProps {
provider: string;
rows: CostByProviderModel[];
/** company monthly budget in cents (0 means unlimited) */
budgetMonthlyCents: number;
/** Only compare a current-month report with the monthly budget. */
showBudgetUtilization?: boolean;
/** total company spend in this period in cents, all providers */
totalCompanySpendCents: number;
/** spend in the current calendar week in cents, this provider only */
weekSpendCents: number;
/** rolling window rows for this provider: 5h, 24h, 7d */
windowRows: CostWindowSpendRow[];
showDeficitNotch: boolean;
/** live subscription quota windows from the provider's own api */
quotaWindows?: QuotaWindow[];
quotaAccounts?: ProviderQuotaResult[];
quotaRequestFailed?: boolean;
quotaError?: string | null;
quotaSource?: string | null;
quotaLoading?: boolean;
}
export function ProviderQuotaCard({
provider,
rows,
budgetMonthlyCents,
showBudgetUtilization = true,
totalCompanySpendCents,
weekSpendCents,
windowRows,
showDeficitNotch,
quotaAccounts,
quotaRequestFailed,
quotaWindows = [],
quotaError = null,
quotaSource = null,
quotaLoading = false,
}: ProviderQuotaCardProps) {
// single-pass aggregation over rows — memoized so the 8 derived values are not
// recomputed on every parent render tick (providers tab polls every 30s, and each
// card is mounted twice: once in the "all" tab grid and once in its per-provider tab).
const totals = useMemo(() => {
let inputTokens = 0, outputTokens = 0, costCents = 0;
let apiRunCount = 0, subRunCount = 0, subInputTokens = 0, subOutputTokens = 0;
let eventCount = 0, estimatedEventCount = 0, unpricedEventCount = 0;
for (const r of rows) {
inputTokens += r.inputTokens + r.cachedInputTokens;
outputTokens += r.outputTokens;
costCents += r.costCents;
eventCount += r.eventCount ?? 0;
estimatedEventCount += r.estimatedEventCount ?? 0;
unpricedEventCount += r.unpricedEventCount ?? 0;
apiRunCount += r.apiRunCount;
subRunCount += r.subscriptionRunCount;
subInputTokens += r.subscriptionInputTokens + r.subscriptionCachedInputTokens;
subOutputTokens += r.subscriptionOutputTokens;
}
const totalTokens = inputTokens + outputTokens;
const subTokens = subInputTokens + subOutputTokens;
// Subscription tokens are already included in the ledger totals.
const allTokens = totalTokens;
return {
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
totalTokens,
totalCostCents: costCents,
eventCount, estimatedEventCount, unpricedEventCount,
totalApiRuns: apiRunCount,
totalSubRuns: subRunCount,
totalSubInputTokens: subInputTokens,
totalSubOutputTokens: subOutputTokens,
totalSubTokens: subTokens,
subSharePct: allTokens > 0 ? (subTokens / allTokens) * 100 : 0,
};
}, [rows]);
const {
totalInputTokens,
totalOutputTokens,
totalTokens,
totalCostCents,
totalApiRuns,
totalSubRuns,
totalSubInputTokens,
totalSubOutputTokens,
totalSubTokens,
subSharePct,
} = totals;
// Each biller contributes to the company budget; there is no per-biller allocation.
const providerBudgetShare = budgetMonthlyCents;
const budgetPct =
providerBudgetShare > 0
? Math.min(100, (totalCostCents / providerBudgetShare) * 100)
: 0;
// 4.33 = average weeks per calendar month (52 / 12)
const weeklyBudgetShare = providerBudgetShare > 0 ? providerBudgetShare / 4.33 : 0;
const weekPct =
weeklyBudgetShare > 0 ? Math.min(100, (weekSpendCents / weeklyBudgetShare) * 100) : 0;
const hasBudget = showBudgetUtilization && budgetMonthlyCents > 0;
// memoized so the Map and max are not reconstructed on every parent render tick
const windowMap = useMemo(
() => new Map(windowRows.map((r) => [r.window, r])),
[windowRows],
);
const maxWindowCents = useMemo(
() => Math.max(...windowRows.map((r) => r.costCents), 0),
[windowRows],
);
const isClaudeQuotaPanel = provider === "anthropic";
const isCodexQuotaPanel = provider === "openai" && quotaSource?.startsWith("codex-");
const supportsSubscriptionQuota = provider === "anthropic" || provider === "openai";
const showSubscriptionQuotaSection =
supportsSubscriptionQuota && (quotaLoading || Boolean(quotaAccounts?.length) || quotaWindows.length > 0 || quotaError != null);
return (
Rolling windows
Subscription
{totalSubRuns} runs
{" · "}
{totalSubTokens > 0 && (
<>
{formatTokens(totalSubTokens)} total
{" · "}
>
)}
{formatTokens(totalSubInputTokens)} in
{" · "}
{formatTokens(totalSubOutputTokens)} out
{Math.round(subSharePct)}% of token usage via subscription
Subscription quota
{quotaUnavailableMessage(quotaWindows.length > 0)}
{qw.detail}
resets {new Date(qw.resetsAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })}