diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx index 09bdfe91e2..8384840b0b 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx @@ -30,6 +30,8 @@ interface QuotaCardProps { providerLabel: string; onRefresh: () => void; onOpenCutoff: () => void; + onToggleActive: (nextActive: boolean) => void; + togglingActive: boolean; } export default function QuotaCard({ @@ -42,7 +44,10 @@ export default function QuotaCard({ providerLabel, onRefresh, onOpenCutoff, + onToggleActive, + togglingActive, }: QuotaCardProps) { + const isActive = connection.isActive ?? true; const quotas = quota?.quotas ?? []; const cardStatus = useMemo(() => worstStatus(quotas), [quotas]); const tierMeta = useMemo( @@ -66,7 +71,7 @@ export default function QuotaCard({ return ( ; onRefresh: (id: string, provider: string) => void; onOpenCutoff: (connection: any) => void; + onToggleActive: (id: string, nextActive: boolean) => void; + togglingActiveId: string | null; } export default function QuotaCardGrid({ @@ -24,6 +26,8 @@ export default function QuotaCardGrid({ providerLabels, onRefresh, onOpenCutoff, + onToggleActive, + togglingActiveId, }: Props) { if (connections.length === 0) return null; @@ -58,6 +62,8 @@ export default function QuotaCardGrid({ providerLabel={providerLabels[conn.provider] || conn.provider} onRefresh={() => onRefresh(conn.id, conn.provider)} onOpenCutoff={() => onOpenCutoff(conn)} + onToggleActive={(nextActive) => onToggleActive(conn.id, nextActive)} + togglingActive={togglingActiveId === conn.id} /> ))} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx index 968c189fe9..fc13c6578d 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx @@ -16,6 +16,7 @@ import { CardSkeleton } from "@/shared/components/Loading"; import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers"; import { pickDisplayValue } from "@/shared/utils/maskEmail"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; +import { useNotificationStore } from "@/store/notificationStore"; import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle"; import QuotaCutoffModal from "./QuotaCutoffModal"; import QuotaCardGrid from "./QuotaCardGrid"; @@ -233,7 +234,9 @@ export default function ProviderLimits({ [t] ); const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible); + const notify = useNotificationStore(); const [connections, setConnections] = useState([]); + const [togglingActiveId, setTogglingActiveId] = useState(null); const [quotaData, setQuotaData] = useState>({}); const [loading, setLoading] = useState>({}); const [errors, setErrors] = useState>({}); @@ -321,6 +324,36 @@ export default function ProviderLimits({ } }, []); + // Toggle a connection's active state straight from the quota overview, so an + // operator can park an account that is being routed to despite low quota. + // Mirrors saveQuotaWindowThresholds: PUT /api/providers/[id] + optimistic state. + const handleToggleActive = useCallback( + async (connectionId: string, nextActive: boolean) => { + setTogglingActiveId(connectionId); + try { + const res = await fetch(`/api/providers/${connectionId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ isActive: nextActive }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setConnections((prev) => + prev.map((c) => (c.id === connectionId ? { ...c, isActive: nextActive } : c)) + ); + notify.success( + nextActive + ? tr("accountActivated", "Account activated") + : tr("accountDeactivated", "Account deactivated") + ); + } catch { + notify.error(tr("toggleActiveFailed", "Failed to update account status")); + } finally { + setTogglingActiveId(null); + } + }, + [notify, tr] + ); + const applyCachedQuotaState = useCallback( (connectionList: any[], caches: Record) => { const nextQuotaData: Record = {}; @@ -985,6 +1018,8 @@ export default function ProviderLimits({ setCutoffModalWindows(windows); setCutoffModalConn(conn); }} + onToggleActive={handleToggleActive} + togglingActiveId={togglingActiveId} /> diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx index 2b3cf96acb..a76a99063c 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx @@ -20,6 +20,10 @@ interface Props { onRefresh: () => void; onOpenCutoff: () => void; hasCutoffOverrides: boolean; + /** Toggle the connection's active state (routing on/off). */ + onToggleActive: (nextActive: boolean) => void; + /** True while the active-state PUT is in flight. */ + togglingActive: boolean; } export default function QuotaCardHeader({ @@ -34,8 +38,14 @@ export default function QuotaCardHeader({ onRefresh, onOpenCutoff, hasCutoffOverrides, + onToggleActive, + togglingActive, }: Props) { const t = useTranslations("usage"); + const isActive = connection.isActive ?? true; + const toggleActiveLabel = isActive + ? translateUsageOrFallback(t, "deactivateAccount", "Deactivate account (stop routing)") + : translateUsageOrFallback(t, "activateAccount", "Activate account (resume routing)"); const accountName = pickDisplayValue( [connection.name, connection.displayName, connection.email], emailsVisible, @@ -59,9 +69,7 @@ export default function QuotaCardHeader({ time: tokenCountdown, }) : translateUsageOrFallback(t, "tokenExpired", "Token expired"); - const tokenExpiryTitle = hasTokenExpiry - ? new Date(tokenExpiryMs).toLocaleString() - : undefined; + const tokenExpiryTitle = hasTokenExpiry ? new Date(tokenExpiryMs).toLocaleString() : undefined; return (
@@ -118,6 +126,24 @@ export default function QuotaCardHeader({
+