@burdenoff/microfe-billing 2026.815.1 → 2026.825.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/BillingRoot.js.map +1 -1
- package/dist/billing/BillingUserRoutes.js.map +1 -1
- package/dist/billing/hooks/useBillingAccountSelection.js.map +1 -1
- package/dist/billing/hooks/useBillingEventEmitter.js.map +1 -1
- package/dist/billing/hooks/useBillingNavigate.js.map +1 -1
- package/dist/billing/hooks/useBillingPermissions.js.map +1 -1
- package/dist/billing/hooks/useDefaultBillingCurrency.js.map +1 -1
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js.map +1 -1
- package/dist/billing/modules/addons/hooks/useAddons.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
- package/dist/billing/modules/addons/store/addonCartStore.js.map +1 -1
- package/dist/billing/modules/affiliate/api.js.map +1 -1
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +107 -107
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js +80 -81
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useInvoices.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useRefunds.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useTransactions.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/UserRefundsPage.js +127 -127
- package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/NativeCheckoutView.js.map +1 -1
- package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
- package/dist/billing/modules/checkout/components/PromoCodeInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js.map +1 -1
- package/dist/billing/modules/checkout/components/StoreShippingSection.js.map +1 -1
- package/dist/billing/modules/checkout/components/StripePaymentForm.js.map +1 -1
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js +0 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useNativeCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/usePaymentProviders.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/usePaymentStatusPolling.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useStoreShipping.js.map +1 -1
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +412 -395
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/checkout/utils/storeOrder.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
- package/dist/billing/modules/credits/hooks/useCredits.js.map +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/hooks/useBillingOverviewData.js.map +1 -1
- package/dist/billing/modules/dashboard/hooks/useDashboard.js.map +1 -1
- package/dist/billing/modules/dashboard/pages/OverviewPage.js +1 -1
- package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
- package/dist/billing/modules/earnings/api.js.map +1 -1
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js +145 -145
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/EarningsPage.js +133 -133
- package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +7 -9
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutsPage.js +95 -95
- package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
- package/dist/billing/modules/plans/constants/featuredQuotas.js.map +1 -1
- package/dist/billing/modules/plans/hooks/usePlans.js.map +1 -1
- package/dist/billing/modules/plans/hooks/useQuotas.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
- package/dist/billing/modules/quota/hooks/useQuotaTemplates.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaAssignFreePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
- package/dist/billing/modules/store/pages/StoreCheckoutPage.js +5 -6
- package/dist/billing/modules/store/pages/StoreCheckoutPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js.map +1 -1
- package/dist/billing/modules/subscriptions/hooks/useSubscriptions.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/modules/usage/hooks/useUsage.js.map +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/providers/BillingProvider.js.map +1 -1
- package/dist/billing/shared/components/AccessDenied.js.map +1 -1
- package/dist/billing/shared/components/ActorIdentity.js.map +1 -1
- package/dist/billing/shared/components/CurrencyPriceOverridesEditor.js.map +1 -1
- package/dist/billing/shared/components/EmptyState.js.map +1 -1
- package/dist/billing/shared/components/PageHeader.js.map +1 -1
- package/dist/billing/shared/components/ServerError.js.map +1 -1
- package/dist/billing/shared/components/StatCard.js.map +1 -1
- package/dist/billing/shared/hooks/useActorProfiles.js.map +1 -1
- package/dist/billing/shared/types/graphql.js.map +1 -1
- package/dist/billing/shared/ui/Card.js.map +1 -1
- package/dist/billing/shared/ui/MetricCard.js.map +1 -1
- package/dist/billing/shared/ui/ProgressBar.js.map +1 -1
- package/dist/billing/shared/ui/StatusPill.js.map +1 -1
- package/dist/billing/shared/utils/directBillingGraphql.js.map +1 -1
- package/dist/billing/shared/utils/error.js.map +1 -1
- package/dist/billing/shared/utils/format.js.map +1 -1
- package/dist/billing/shared/utils/gatewayGraphql.js.map +1 -1
- package/dist/billing/shared/utils/navigation.js.map +1 -1
- package/dist/billing/shared/utils/paymentTheme.js.map +1 -1
- package/dist/billing/shared/utils/planFeatureQuantity.js.map +1 -1
- package/dist/billing/shared/utils/status.js.map +1 -1
- package/dist/billing/shared/utils/tokens.js.map +1 -1
- package/dist/generated/global-operations.js +1 -1
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.js.map +1 -1
- package/dist/generated/wspace-operations.js +1 -1
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/node_modules/@capacitor/core/dist/index.js +1 -1
- package/dist/node_modules/@capacitor/core/dist/index.js.map +1 -1
- package/package.json +13 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriptionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/pages/SubscriptionsListPage.tsx"],"sourcesContent":["/**\n * Subscriptions Module - Subscriptions List Page\n * Displays paginated subscriptions for the selected billing account\n */\n\nimport { useState, useEffect, useRef, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingAccounts, usePaginatedSubscriptions } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { BillingSubscription, SubscriptionStatus, BillingAccount } from '../../../shared/types';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport {\n formatCurrency,\n formatDate,\n formatPlanDuration,\n formatSubscriptionStatus,\n} from '../../../shared/utils/format';\nimport { getSubscriptionStatusColor } from '../../../shared/utils/status';\nimport { StatusPill } from '../../../shared/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, EmphasisPanel, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\ntype FilterStatus = 'all' | 'active' | 'canceled' | 'inactive';\n\n// Statuses that count as \"inactive\" (historical/past subscriptions)\nconst INACTIVE_STATUSES = ['expired', 'upgraded'] as const;\n\nconst PAGE_SIZE = 10;\n\nexport const SubscriptionsListPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { orgId } = useBilling();\n\n // Fetch all billing accounts for the selector\n const { billingAccounts, isLoading: isLoadingAccounts } = useBillingAccounts();\n\n // State\n const {\n selectedAccountId: selectedBillingAccountId,\n setSelectedAccountId: setSelectedBillingAccountId,\n } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n\n const [isAccountSelectorOpen, setIsAccountSelectorOpen] = useState(false);\n const [filterStatus, setFilterStatus] = useState<FilterStatus>('all');\n const [searchQuery, setSearchQuery] = useState('');\n const [debouncedSearch, setDebouncedSearch] = useState('');\n const [currentPage, setCurrentPage] = useState(1);\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n useEffect(() => {\n clearTimeout(debounceTimer.current);\n debounceTimer.current = setTimeout(() => {\n setDebouncedSearch(searchQuery);\n setCurrentPage(1);\n }, 400);\n return () => clearTimeout(debounceTimer.current);\n }, [searchQuery]);\n\n // Fetch paginated subscriptions for selected billing account\n const {\n subscriptions,\n activeSubscriptions,\n totalCount,\n totalPages,\n isLoading: isLoadingSubscriptions,\n error,\n refetch,\n } = usePaginatedSubscriptions(\n selectedBillingAccountId ?? undefined,\n currentPage,\n PAGE_SIZE,\n debouncedSearch || undefined\n );\n\n // Get selected billing account info from the list\n const selectedBillingAccount = billingAccounts.find((a) => a.id === selectedBillingAccountId);\n\n // Handle account selection\n const handleSelectAccount = (account: BillingAccount) => {\n setSelectedBillingAccountId(account.id);\n setIsAccountSelectorOpen(false);\n setCurrentPage(1); // Reset to first page when switching accounts\n };\n\n // Handle page change\n const handlePageChange = (newPage: number) => {\n setCurrentPage(newPage);\n };\n\n // Client-side status filter only; search is handled by the backend\n const filteredSubscriptions = subscriptions.filter((subscription) => {\n if (filterStatus === 'all') return true;\n if (filterStatus === 'inactive') {\n return INACTIVE_STATUSES.includes(subscription.status as (typeof INACTIVE_STATUSES)[number]);\n }\n return subscription.status === filterStatus;\n });\n\n const isLoading = isLoadingAccounts || isLoadingSubscriptions;\n\n // Permission check\n if (!permissions.canViewSubscriptions) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-bg-sunken flex items-center justify-center\">\n <svg\n className=\"size-6 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">\n You don't have permission to view subscriptions.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (isLoading && !selectedBillingAccount) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border-subtle rounded-lg p-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-12 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"flex-1 space-y-2\">\n <div className=\"h-5 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-6 text-status-error-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Failed to load subscriptions</h2>\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.subscriptions.title', 'Subscriptions')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.subscriptions.subtitle', 'Manage your active and past subscriptions')}\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.subscriptions.purpose',\n 'See every plan a billing account is subscribed to — active, canceled and expired — in one place. Use it to review what you are paying for, when each plan renews or ends, upgrade or cancel a subscription, and start a new one by browsing plans.'\n )}\n </PagePurpose>\n </div>\n\n {permissions.canCreateSubscription && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 4v16m8-8H4\"\n />\n </svg>\n New Subscription\n </button>\n )}\n </div>\n\n {/* Account Selector (if multiple accounts) */}\n {billingAccounts.length > 1 && (\n <div className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setIsAccountSelectorOpen(!isAccountSelectorOpen)}\n className=\"w-full sm:w-auto flex items-center justify-between gap-3 px-4 py-3 border border-border-seam rounded-card bg-bg-surface hover:bg-bg-sunken hover:border-border-strong transition-all duration-200\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"size-9 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center\">\n <svg\n className=\"size-4 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n <div className=\"text-left\">\n <p className=\"text-sm font-medium text-text-primary\">\n {selectedBillingAccount?.name || 'Select Account'}\n </p>\n <p className=\"text-xs text-text-secondary\">{selectedBillingAccount?.email}</p>\n </div>\n </div>\n <svg\n className={`size-4 text-text-secondary transition-transform ${isAccountSelectorOpen ? 'rotate-180' : ''}`}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </button>\n\n {isAccountSelectorOpen && (\n <>\n <div className=\"fixed inset-0 z-10\" onClick={() => setIsAccountSelectorOpen(false)} />\n <div className=\"absolute top-full left-0 right-0 sm:right-auto mt-1 w-full sm:w-80 bg-bg-elevated border border-border-seam rounded-card shadow-elevation-3 z-20 py-1 max-h-60 overflow-y-auto\">\n {billingAccounts.map((account) => (\n <button\n type=\"button\"\n key={account.id}\n onClick={() => handleSelectAccount(account)}\n className=\"w-full flex items-center justify-between gap-3 px-4 py-3 hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"size-9 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <svg\n className=\"size-4 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n <div className=\"text-left min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {account.name}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{account.email}</p>\n </div>\n </div>\n {selectedBillingAccount?.id === account.id && (\n <svg\n className=\"size-4 text-text-link flex-shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </button>\n ))}\n </div>\n </>\n )}\n </div>\n )}\n\n {/* Account Summary — primary emphasis zone */}\n {selectedBillingAccount && (\n <EmphasisPanel className=\"p-5\">\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">Account</p>\n <p className=\"text-lg font-semibold text-text-primary mt-1\">\n {selectedBillingAccount.name}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Active Subscriptions</p>\n <p className=\"text-lg font-semibold tabular-nums text-status-success-text mt-1\">\n {activeSubscriptions.length}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Credit Balance</p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary mt-1\">\n {(selectedBillingAccount.creditAmount || 0).toLocaleString()} Credits\n </p>\n </div>\n </div>\n </EmphasisPanel>\n )}\n\n {/* Filters */}\n <div className=\"flex flex-col sm:flex-row flex-wrap items-stretch sm:items-center gap-3 sm:gap-4 pb-4 border-b border-border-subtle\">\n {/* Status Filter */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken flex-wrap\">\n {(['all', 'active', 'canceled', 'inactive'] as FilterStatus[]).map((status) => (\n <button\n type=\"button\"\n key={status}\n onClick={() => setFilterStatus(status)}\n className={`px-3 py-1.5 text-sm font-medium rounded-button transition-colors capitalize ${\n filterStatus === status\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {status === 'inactive' ? 'Inactive' : status}\n </button>\n ))}\n </div>\n\n {/* Search */}\n <div className=\"relative flex-1 min-w-0 w-full sm:max-w-md\">\n <svg\n className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n />\n </svg>\n <input\n type=\"text\"\n placeholder={tr('billing.subscriptions.searchPlaceholder', 'Search subscriptions...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2 text-sm border border-border-subtle rounded-button bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-transparent\"\n />\n </div>\n\n {/* Total Count */}\n <div className=\"text-sm text-text-secondary\">\n {totalCount} subscription{totalCount !== 1 ? 's' : ''} total\n </div>\n </div>\n\n {/* Subscriptions List */}\n {filteredSubscriptions.length === 0 ? (\n <IllustratedEmptyState\n illustration={filterStatus !== 'all' || debouncedSearch ? 'empty-search' : 'empty-data'}\n title={tr('billing.subscriptions.noSubscriptionsFound', 'No subscriptions found')}\n description={\n filterStatus !== 'all' || debouncedSearch\n ? 'No subscriptions match your filters.'\n : \"You don't have any subscriptions yet.\"\n }\n action={\n permissions.canCreateSubscription && filterStatus === 'all' && !debouncedSearch ? (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Browse Plans\n </button>\n ) : undefined\n }\n />\n ) : (\n <div className=\"space-y-4\">\n {filteredSubscriptions.map((subscription) => (\n <SubscriptionCard\n key={subscription.id}\n subscription={subscription}\n onView={() =>\n navigateTo(\n withBillingAccountId(\n `/subscriptions/${subscription.id}`,\n subscription.billingAccountId\n )\n )\n }\n canCancel={permissions.canCancelSubscription}\n />\n ))}\n </div>\n )}\n\n {/* Pagination */}\n {totalPages > 1 && (\n <div className=\"flex items-center justify-center gap-2 pt-4\">\n <button\n type=\"button\"\n onClick={() => handlePageChange(currentPage - 1)}\n disabled={currentPage === 1}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n\n <div className=\"flex items-center gap-1\">\n {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => {\n // Show first, last, current, and adjacent pages\n const showPage =\n page === 1 || page === totalPages || Math.abs(page - currentPage) <= 1;\n\n // Show ellipsis\n const showEllipsisBefore = page === currentPage - 2 && currentPage > 3;\n const showEllipsisAfter = page === currentPage + 2 && currentPage < totalPages - 2;\n\n if (showEllipsisBefore || showEllipsisAfter) {\n return (\n <span key={page} className=\"px-2 text-text-secondary\">\n ...\n </span>\n );\n }\n\n if (!showPage) return null;\n\n return (\n <button\n type=\"button\"\n key={page}\n onClick={() => handlePageChange(page)}\n className={`px-3 py-1.5 text-sm rounded-button transition-colors ${\n page === currentPage\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'border border-border-subtle hover:bg-bg-sunken'\n }`}\n >\n {page}\n </button>\n );\n })}\n </div>\n\n <button\n type=\"button\"\n onClick={() => handlePageChange(currentPage + 1)}\n disabled={currentPage === totalPages}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Subscription Card Component\n// ============================================================================\n\ninterface SubscriptionCardProps {\n subscription: BillingSubscription;\n onView: () => void;\n canCancel: boolean;\n}\n\nconst SubscriptionCard: FC<SubscriptionCardProps> = ({\n subscription,\n onView,\n canCancel: _canCancel,\n}) => {\n const plan = subscription.plan;\n const statusColor = getSubscriptionStatusColor(subscription.status);\n const isActive = subscription.status === ('active' as SubscriptionStatus);\n\n // Calculate days remaining\n const endDate = new Date(subscription.endDate);\n const now = new Date();\n const daysRemaining = Math.max(\n 0,\n Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))\n );\n\n return (\n <div\n className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 hover:border-border-strong hover:shadow-elevation-2 transition-all duration-200 cursor-pointer\"\n onClick={onView}\n >\n <div className=\"p-5\">\n <div className=\"flex flex-col sm:flex-row sm:items-center gap-4\">\n {/* Plan Icon */}\n <div className=\"size-12 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center shrink-0\">\n <svg\n className=\"size-6 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"\n />\n </svg>\n </div>\n\n {/* Subscription Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <h3 className=\"font-semibold text-text-primary\">{plan?.name || 'Unknown Plan'}</h3>\n <StatusPill status={statusColor} dot pulse={isActive}>\n {formatSubscriptionStatus(subscription.status)}\n </StatusPill>\n </div>\n <div className=\"flex items-center gap-4 mt-1 text-sm text-text-secondary\">\n <span>\n {plan ? formatCurrency(plan.price, plan.currency) : '—'} /{' '}\n {plan ? formatPlanDuration(plan.duration) : '—'}\n </span>\n <span className=\"hidden sm:inline\">•</span>\n <span className=\"hidden sm:inline\">\n Started {formatDate(subscription.startDate, 'short')}\n </span>\n </div>\n </div>\n\n {/* Right Side Info */}\n <div className=\"flex items-center gap-4\">\n {isActive && (\n <div className=\"text-right\">\n <p className=\"text-sm font-medium text-text-primary\">{daysRemaining} days</p>\n <p className=\"text-xs text-text-secondary\">remaining</p>\n </div>\n )}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onView();\n }}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken hover:border-border-strong transition-all duration-200\"\n >\n View\n </button>\n </div>\n </div>\n\n {/* Additional Info */}\n {subscription.addonSubscriptions && subscription.addonSubscriptions.length > 0 && (\n <div className=\"mt-4 pt-4 border-t border-border-subtle\">\n <p className=\"text-sm text-text-secondary\">\n {subscription.addonSubscriptions.length} addon\n {subscription.addonSubscriptions.length !== 1 ? 's' : ''} attached\n </p>\n </div>\n )}\n\n {/* Cancellation Info */}\n {subscription.canceledAt && (\n <div className=\"mt-4 pt-4 border-t border-border-subtle\">\n <p className=\"text-sm text-text-secondary\">\n Canceled on {formatDate(subscription.canceledAt)}\n {subscription.cancellationReason && <span>: {subscription.cancellationReason}</span>}\n </p>\n </div>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SubscriptionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/pages/SubscriptionsListPage.tsx"],"sourcesContent":["/**\n * Subscriptions Module - Subscriptions List Page\n * Displays paginated subscriptions for the selected billing account\n */\n\nimport { useState, useEffect, useRef, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingAccounts, usePaginatedSubscriptions } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { BillingSubscription, SubscriptionStatus, BillingAccount } from '../../../shared/types';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport {\n formatCurrency,\n formatDate,\n formatPlanDuration,\n formatSubscriptionStatus,\n} from '../../../shared/utils/format';\nimport { getSubscriptionStatusColor } from '../../../shared/utils/status';\nimport { StatusPill } from '../../../shared/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, EmphasisPanel, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\ntype FilterStatus = 'all' | 'active' | 'canceled' | 'inactive';\n\n// Statuses that count as \"inactive\" (historical/past subscriptions)\nconst INACTIVE_STATUSES = ['expired', 'upgraded'] as const;\n\nconst PAGE_SIZE = 10;\n\nexport const SubscriptionsListPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { orgId } = useBilling();\n\n // Fetch all billing accounts for the selector\n const { billingAccounts, isLoading: isLoadingAccounts } = useBillingAccounts();\n\n // State\n const {\n selectedAccountId: selectedBillingAccountId,\n setSelectedAccountId: setSelectedBillingAccountId,\n } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n\n const [isAccountSelectorOpen, setIsAccountSelectorOpen] = useState(false);\n const [filterStatus, setFilterStatus] = useState<FilterStatus>('all');\n const [searchQuery, setSearchQuery] = useState('');\n const [debouncedSearch, setDebouncedSearch] = useState('');\n const [currentPage, setCurrentPage] = useState(1);\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n useEffect(() => {\n clearTimeout(debounceTimer.current);\n debounceTimer.current = setTimeout(() => {\n setDebouncedSearch(searchQuery);\n setCurrentPage(1);\n }, 400);\n return () => clearTimeout(debounceTimer.current);\n }, [searchQuery]);\n\n // Fetch paginated subscriptions for selected billing account\n const {\n subscriptions,\n activeSubscriptions,\n totalCount,\n totalPages,\n isLoading: isLoadingSubscriptions,\n error,\n refetch,\n } = usePaginatedSubscriptions(\n selectedBillingAccountId ?? undefined,\n currentPage,\n PAGE_SIZE,\n debouncedSearch || undefined\n );\n\n // Get selected billing account info from the list\n const selectedBillingAccount = billingAccounts.find((a) => a.id === selectedBillingAccountId);\n\n // Handle account selection\n const handleSelectAccount = (account: BillingAccount) => {\n setSelectedBillingAccountId(account.id);\n setIsAccountSelectorOpen(false);\n setCurrentPage(1); // Reset to first page when switching accounts\n };\n\n // Handle page change\n const handlePageChange = (newPage: number) => {\n setCurrentPage(newPage);\n };\n\n // Client-side status filter only; search is handled by the backend\n const filteredSubscriptions = subscriptions.filter((subscription) => {\n if (filterStatus === 'all') return true;\n if (filterStatus === 'inactive') {\n return INACTIVE_STATUSES.includes(subscription.status as (typeof INACTIVE_STATUSES)[number]);\n }\n return subscription.status === filterStatus;\n });\n\n const isLoading = isLoadingAccounts || isLoadingSubscriptions;\n\n // Permission check\n if (!permissions.canViewSubscriptions) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-bg-sunken flex items-center justify-center\">\n <svg\n className=\"size-6 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">\n You don't have permission to view subscriptions.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (isLoading && !selectedBillingAccount) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border-subtle rounded-lg p-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-12 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"flex-1 space-y-2\">\n <div className=\"h-5 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-6 text-status-error-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Failed to load subscriptions</h2>\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.subscriptions.title', 'Subscriptions')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.subscriptions.subtitle', 'Manage your active and past subscriptions')}\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.subscriptions.purpose',\n 'See every plan a billing account is subscribed to — active, canceled and expired — in one place. Use it to review what you are paying for, when each plan renews or ends, upgrade or cancel a subscription, and start a new one by browsing plans.'\n )}\n </PagePurpose>\n </div>\n\n {permissions.canCreateSubscription && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 4v16m8-8H4\"\n />\n </svg>\n New Subscription\n </button>\n )}\n </div>\n\n {/* Account Selector (if multiple accounts) */}\n {billingAccounts.length > 1 && (\n <div className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setIsAccountSelectorOpen(!isAccountSelectorOpen)}\n className=\"w-full sm:w-auto flex items-center justify-between gap-3 px-4 py-3 border border-border-seam rounded-card bg-bg-surface hover:bg-bg-sunken hover:border-border-strong transition-all duration-200\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"size-9 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center\">\n <svg\n className=\"size-4 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n <div className=\"text-left\">\n <p className=\"text-sm font-medium text-text-primary\">\n {selectedBillingAccount?.name || 'Select Account'}\n </p>\n <p className=\"text-xs text-text-secondary\">{selectedBillingAccount?.email}</p>\n </div>\n </div>\n <svg\n className={`size-4 text-text-secondary transition-transform ${isAccountSelectorOpen ? 'rotate-180' : ''}`}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </button>\n\n {isAccountSelectorOpen && (\n <>\n <div className=\"fixed inset-0 z-10\" onClick={() => setIsAccountSelectorOpen(false)} />\n <div className=\"absolute top-full left-0 right-0 sm:right-auto mt-1 w-full sm:w-80 bg-bg-elevated border border-border-seam rounded-card shadow-elevation-3 z-20 py-1 max-h-60 overflow-y-auto\">\n {billingAccounts.map((account) => (\n <button\n type=\"button\"\n key={account.id}\n onClick={() => handleSelectAccount(account)}\n className=\"w-full flex items-center justify-between gap-3 px-4 py-3 hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"size-9 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <svg\n className=\"size-4 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n <div className=\"text-left min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {account.name}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{account.email}</p>\n </div>\n </div>\n {selectedBillingAccount?.id === account.id && (\n <svg\n className=\"size-4 text-text-link flex-shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </button>\n ))}\n </div>\n </>\n )}\n </div>\n )}\n\n {/* Account Summary — primary emphasis zone */}\n {selectedBillingAccount && (\n <EmphasisPanel className=\"p-5\">\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">Account</p>\n <p className=\"text-lg font-semibold text-text-primary mt-1\">\n {selectedBillingAccount.name}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Active Subscriptions</p>\n <p className=\"text-lg font-semibold tabular-nums text-status-success-text mt-1\">\n {activeSubscriptions.length}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Credit Balance</p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary mt-1\">\n {(selectedBillingAccount.creditAmount || 0).toLocaleString()} Credits\n </p>\n </div>\n </div>\n </EmphasisPanel>\n )}\n\n {/* Filters */}\n <div className=\"flex flex-col sm:flex-row flex-wrap items-stretch sm:items-center gap-3 sm:gap-4 pb-4 border-b border-border-subtle\">\n {/* Status Filter */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken flex-wrap\">\n {(['all', 'active', 'canceled', 'inactive'] as FilterStatus[]).map((status) => (\n <button\n type=\"button\"\n key={status}\n onClick={() => setFilterStatus(status)}\n className={`px-3 py-1.5 text-sm font-medium rounded-button transition-colors capitalize ${\n filterStatus === status\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {status === 'inactive' ? 'Inactive' : status}\n </button>\n ))}\n </div>\n\n {/* Search */}\n <div className=\"relative flex-1 min-w-0 w-full sm:max-w-md\">\n <svg\n className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n />\n </svg>\n <input\n type=\"text\"\n placeholder={tr('billing.subscriptions.searchPlaceholder', 'Search subscriptions...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2 text-sm border border-border-subtle rounded-button bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-transparent\"\n />\n </div>\n\n {/* Total Count */}\n <div className=\"text-sm text-text-secondary\">\n {totalCount} subscription{totalCount !== 1 ? 's' : ''} total\n </div>\n </div>\n\n {/* Subscriptions List */}\n {filteredSubscriptions.length === 0 ? (\n <IllustratedEmptyState\n illustration={filterStatus !== 'all' || debouncedSearch ? 'empty-search' : 'empty-data'}\n title={tr('billing.subscriptions.noSubscriptionsFound', 'No subscriptions found')}\n description={\n filterStatus !== 'all' || debouncedSearch\n ? 'No subscriptions match your filters.'\n : \"You don't have any subscriptions yet.\"\n }\n action={\n permissions.canCreateSubscription && filterStatus === 'all' && !debouncedSearch ? (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Browse Plans\n </button>\n ) : undefined\n }\n />\n ) : (\n <div className=\"space-y-4\">\n {filteredSubscriptions.map((subscription) => (\n <SubscriptionCard\n key={subscription.id}\n subscription={subscription}\n onView={() =>\n navigateTo(\n withBillingAccountId(\n `/subscriptions/${subscription.id}`,\n subscription.billingAccountId\n )\n )\n }\n canCancel={permissions.canCancelSubscription}\n />\n ))}\n </div>\n )}\n\n {/* Pagination */}\n {totalPages > 1 && (\n <div className=\"flex items-center justify-center gap-2 pt-4\">\n <button\n type=\"button\"\n onClick={() => handlePageChange(currentPage - 1)}\n disabled={currentPage === 1}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n\n <div className=\"flex items-center gap-1\">\n {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => {\n // Show first, last, current, and adjacent pages\n const showPage =\n page === 1 || page === totalPages || Math.abs(page - currentPage) <= 1;\n\n // Show ellipsis\n const showEllipsisBefore = page === currentPage - 2 && currentPage > 3;\n const showEllipsisAfter = page === currentPage + 2 && currentPage < totalPages - 2;\n\n if (showEllipsisBefore || showEllipsisAfter) {\n return (\n <span key={page} className=\"px-2 text-text-secondary\">\n ...\n </span>\n );\n }\n\n if (!showPage) return null;\n\n return (\n <button\n type=\"button\"\n key={page}\n onClick={() => handlePageChange(page)}\n className={`px-3 py-1.5 text-sm rounded-button transition-colors ${\n page === currentPage\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'border border-border-subtle hover:bg-bg-sunken'\n }`}\n >\n {page}\n </button>\n );\n })}\n </div>\n\n <button\n type=\"button\"\n onClick={() => handlePageChange(currentPage + 1)}\n disabled={currentPage === totalPages}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Subscription Card Component\n// ============================================================================\n\ninterface SubscriptionCardProps {\n subscription: BillingSubscription;\n onView: () => void;\n canCancel: boolean;\n}\n\nconst SubscriptionCard: FC<SubscriptionCardProps> = ({\n subscription,\n onView,\n canCancel: _canCancel,\n}) => {\n const plan = subscription.plan;\n const statusColor = getSubscriptionStatusColor(subscription.status);\n const isActive = subscription.status === ('active' as SubscriptionStatus);\n\n // Calculate days remaining\n const endDate = new Date(subscription.endDate);\n const now = new Date();\n const daysRemaining = Math.max(\n 0,\n Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))\n );\n\n return (\n <div\n className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 hover:border-border-strong hover:shadow-elevation-2 transition-all duration-200 cursor-pointer\"\n onClick={onView}\n >\n <div className=\"p-5\">\n <div className=\"flex flex-col sm:flex-row sm:items-center gap-4\">\n {/* Plan Icon */}\n <div className=\"size-12 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center shrink-0\">\n <svg\n className=\"size-6 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"\n />\n </svg>\n </div>\n\n {/* Subscription Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <h3 className=\"font-semibold text-text-primary\">{plan?.name || 'Unknown Plan'}</h3>\n <StatusPill status={statusColor} dot pulse={isActive}>\n {formatSubscriptionStatus(subscription.status)}\n </StatusPill>\n </div>\n <div className=\"flex items-center gap-4 mt-1 text-sm text-text-secondary\">\n <span>\n {plan ? formatCurrency(plan.price, plan.currency) : '—'} /{' '}\n {plan ? formatPlanDuration(plan.duration) : '—'}\n </span>\n <span className=\"hidden sm:inline\">•</span>\n <span className=\"hidden sm:inline\">\n Started {formatDate(subscription.startDate, 'short')}\n </span>\n </div>\n </div>\n\n {/* Right Side Info */}\n <div className=\"flex items-center gap-4\">\n {isActive && (\n <div className=\"text-right\">\n <p className=\"text-sm font-medium text-text-primary\">{daysRemaining} days</p>\n <p className=\"text-xs text-text-secondary\">remaining</p>\n </div>\n )}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onView();\n }}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken hover:border-border-strong transition-all duration-200\"\n >\n View\n </button>\n </div>\n </div>\n\n {/* Additional Info */}\n {subscription.addonSubscriptions && subscription.addonSubscriptions.length > 0 && (\n <div className=\"mt-4 pt-4 border-t border-border-subtle\">\n <p className=\"text-sm text-text-secondary\">\n {subscription.addonSubscriptions.length} addon\n {subscription.addonSubscriptions.length !== 1 ? 's' : ''} attached\n </p>\n </div>\n )}\n\n {/* Cancellation Info */}\n {subscription.canceledAt && (\n <div className=\"mt-4 pt-4 border-t border-border-subtle\">\n <p className=\"text-sm text-text-secondary\">\n Canceled on {formatDate(subscription.canceledAt)}\n {subscription.cancellationReason && <span>: {subscription.cancellationReason}</span>}\n </p>\n </div>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpgradeSubscriptionPage.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.tsx"],"sourcesContent":["/**\n * Change Plan Page\n * Allows users to change their current subscription to a different plan\n * Shows plan features in collapsible sections for easy comparison\n */\n\nimport { type FC, useState, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport {\n ArrowLeft,\n ArrowRight,\n Loader2,\n Package,\n CheckCircle,\n AlertCircle,\n ChevronDown,\n ChevronUp,\n Sparkles,\n Infinity as InfinityIcon,\n} from 'lucide-react';\n\nimport { useSubscription } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { formatCurrency, formatPlanDuration } from '../../../shared/utils/format';\nimport {\n useGetAllPlansQuery,\n useGetDefaultBillingAccountDashboardQuery,\n} from '../../../../generated/global-operations';\nimport type { Plan, PlanDuration, SubscriptionStatus } from '../../../shared/types';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\n\n// Helper type for plan features\ninterface PlanFeature {\n id: string;\n quotaId?: string;\n quota?: {\n id: string;\n name: string;\n limits?: Record<string, number | string | boolean>;\n reusable?: boolean;\n quotaType?: string;\n };\n}\n\n// Helper function to format limit values nicely\nconst formatLimitValue = (value: number | string | boolean): string => {\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (typeof value === 'number') {\n if (value === -1 || value === Infinity) return 'Unlimited';\n if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;\n if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;\n return value.toLocaleString();\n }\n return String(value);\n};\n\n// Helper function to get a user-friendly limit key name\nconst formatLimitKey = (key: string): string => {\n return key\n .replace(/_/g, ' ')\n .replace(/([A-Z])/g, ' $1')\n .replace(/^./, (str) => str.toUpperCase())\n .trim();\n};\n\n// Helper function to aggregate features by quotaId\n// Combines limits for features with the same quota\nconst aggregateFeaturesByQuota = (features: PlanFeature[]): PlanFeature[] => {\n const quotaMap = new Map<string, PlanFeature>();\n\n for (const feature of features) {\n const quotaId = feature.quotaId || feature.quota?.id;\n if (!quotaId || !feature.quota) continue;\n\n const existing = quotaMap.get(quotaId);\n if (existing && existing.quota) {\n // Aggregate limits - sum numeric values\n const existingLimits = existing.quota.limits || {};\n const newLimits = feature.quota.limits || {};\n const combinedLimits: Record<string, number | string | boolean> = { ...existingLimits };\n\n for (const [key, value] of Object.entries(newLimits)) {\n if (typeof value === 'number' && typeof combinedLimits[key] === 'number') {\n // Sum numeric values (but keep -1/unlimited as unlimited)\n const existingVal = combinedLimits[key] as number;\n if (existingVal === -1 || value === -1) {\n combinedLimits[key] = -1; // Unlimited stays unlimited\n } else {\n combinedLimits[key] = existingVal + value;\n }\n } else if (!(key in combinedLimits)) {\n combinedLimits[key] = value;\n }\n }\n\n existing.quota = {\n ...existing.quota,\n limits: combinedLimits,\n };\n } else {\n // Clone the feature to avoid mutating original\n quotaMap.set(quotaId, {\n ...feature,\n quota: feature.quota\n ? {\n ...feature.quota,\n limits: feature.quota.limits ? { ...feature.quota.limits } : undefined,\n }\n : undefined,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n};\n\n// Collapsible Features Component\ninterface CollapsibleFeaturesProps {\n features: PlanFeature[];\n isExpanded: boolean;\n onToggle: () => void;\n variant?: 'current' | 'available';\n}\n\nconst CollapsibleFeatures: FC<CollapsibleFeaturesProps> = ({\n features,\n isExpanded,\n onToggle,\n variant = 'available',\n}) => {\n // Aggregate features with same quotaId\n const aggregatedFeatures = useMemo(() => aggregateFeaturesByQuota(features), [features]);\n const featureCount = aggregatedFeatures.length;\n\n if (featureCount === 0) {\n return <p className=\"text-xs text-text-secondary italic\">No features defined</p>;\n }\n\n return (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggle();\n }}\n className={`flex items-center gap-1.5 text-xs font-medium transition-colors ${\n variant === 'current'\n ? 'text-text-secondary hover:text-text-primary'\n : 'text-text-link hover:text-text-link'\n }`}\n >\n <Sparkles className=\"size-3\" />\n <span>\n {featureCount} feature{featureCount !== 1 ? 's' : ''}\n </span>\n {isExpanded ? <ChevronUp className=\"size-3\" /> : <ChevronDown className=\"size-3\" />}\n </button>\n\n {isExpanded && (\n <div className={`mt-2 space-y-2 ${variant === 'current' ? 'pl-0' : 'pl-1'}`}>\n {aggregatedFeatures.map((feature) => {\n const quota = feature.quota;\n if (!quota) return null;\n\n const limits = quota.limits || {};\n const limitEntries = Object.entries(limits);\n\n return (\n <div\n key={feature.id}\n className={`rounded-button p-2 ${\n variant === 'current' ? 'bg-bg-sunken' : 'bg-[var(--color-accent-soft)]'\n }`}\n >\n <p\n className={`text-xs font-medium ${\n variant === 'current' ? 'text-text-primary' : 'text-text-primary'\n }`}\n >\n {quota.name}\n </p>\n {limitEntries.length > 0 && (\n <div className=\"mt-1 flex flex-wrap gap-x-3 gap-y-1\">\n {limitEntries.map(([key, value]) => (\n <div\n key={key}\n className=\"flex items-center gap-1 text-xs text-text-secondary\"\n >\n <span className=\"opacity-70\">{formatLimitKey(key)}:</span>\n <span className=\"font-medium text-text-primary flex items-center gap-0.5\">\n {value === -1 || value === Infinity ? (\n <>\n <InfinityIcon className=\"size-3\" />\n <span className=\"sr-only\">Unlimited</span>\n </>\n ) : (\n formatLimitValue(value as number | string | boolean)\n )}\n </span>\n </div>\n ))}\n </div>\n )}\n {quota.reusable && (\n <span className=\"inline-block mt-1 text-[10px] px-1.5 py-0.5 rounded bg-status-success-bg-subtle text-status-success-text\">\n Reusable\n </span>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n\nexport const UpgradeSubscriptionPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { subscriptionId } = useParams<{ subscriptionId: string }>();\n const navigateTo = useBillingNavigate();\n const { orgId } = useBilling();\n const permissions = useBillingPermissions();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n\n const { data: defaultBillingAccountData } = useGetDefaultBillingAccountDashboardQuery({\n fetchPolicy: 'cache-first',\n });\n const billingAccountId =\n selectedAccountId ?? defaultBillingAccountData?.getDefaultBillingAccount?.id ?? undefined;\n const { subscription, isLoading: subscriptionLoading } = useSubscription(\n subscriptionId,\n billingAccountId\n );\n\n // Fetch available plans (includes features)\n const { data: plansData, loading: plansLoading } = useGetAllPlansQuery();\n\n // State\n const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);\n const [expandedFeatures, setExpandedFeatures] = useState<Record<string, boolean>>({});\n\n // Get all plans including current for feature lookup\n const allPlans = useMemo(() => {\n return (plansData?.getAllPlans || []) as Plan[];\n }, [plansData]);\n\n // Get current plan with features from allPlans\n const currentPlanWithFeatures = useMemo(() => {\n const currentPlanId = subscription?.planId;\n return allPlans.find((p) => p.id === currentPlanId) || null;\n }, [allPlans, subscription?.planId]);\n\n // Filter plans - show all other active plans (same product if productID exists, otherwise all)\n const availablePlans = useMemo(() => {\n const currentPlanId = subscription?.planId;\n const currentProductId = subscription?.plan?.productID;\n\n return allPlans\n .filter((plan) => {\n // Always exclude current plan\n if (plan.id === currentPlanId) return false;\n // Only show active plans\n if (!plan.isActive) return false;\n // If we have productID, filter by same product\n if (currentProductId) {\n return plan.productID === currentProductId;\n }\n // If no productID, show all active plans\n return true;\n })\n .sort((a, b) => a.price - b.price);\n }, [allPlans, subscription]);\n\n const selectedPlan = useMemo(() => {\n return availablePlans.find((p) => p.id === selectedPlanId) || null;\n }, [availablePlans, selectedPlanId]);\n\n const isDowngrade =\n selectedPlan !== null && selectedPlan.price < (subscription?.plan?.price ?? 0);\n\n // Toggle feature expansion\n const toggleFeatures = (planId: string) => {\n setExpandedFeatures((prev) => ({\n ...prev,\n [planId]: !prev[planId],\n }));\n };\n\n // Handle proceed to checkout\n const handleProceedToCheckout = () => {\n if (!subscription || !selectedPlan) return;\n const currentPrice = subscription.plan?.price ?? 0;\n const isDowngrade = selectedPlan.price < currentPrice;\n navigateTo(`/checkout/${selectedPlan.id}`, {\n state: {\n purchaseType: 'PLAN',\n // Upgrade: old quotas transfer to new plan immediately\n // Downgrade: old plan runs to natural expiry, auto-renewal cancelled\n ...(isDowngrade\n ? { downgradeFromSubscriptionId: subscription.id }\n : { upgradeFromSubscriptionId: subscription.id }),\n currentPlanName: subscription.plan?.name,\n billingAccountId: subscription.billingAccountId,\n },\n });\n };\n\n // Permission check\n if (!permissions.canUpdateSubscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-bg-sunken flex items-center justify-center\">\n <AlertCircle className=\"size-6 text-text-secondary\" />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">\n You don't have permission to change subscriptions.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (subscriptionLoading || plansLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <Loader2 className=\"size-8 animate-spin mx-auto text-text-link\" />\n <p className=\"text-text-secondary\">Loading plans…</p>\n </div>\n </div>\n );\n }\n\n // Subscription not found\n if (!subscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <AlertCircle className=\"size-12 mx-auto text-status-error-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">Subscription not found</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button\"\n >\n {tr('billing.subscriptions.backToSubscriptions', 'Back to Subscriptions')}\n </button>\n </div>\n </div>\n );\n }\n\n // Subscription is not active (already upgraded/canceled/etc)\n const isActive = subscription.status === ('active' as SubscriptionStatus);\n if (!isActive) {\n return (\n <div className=\"max-w-2xl mx-auto p-6 space-y-6\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <div className=\"text-center space-y-4 py-12\">\n <AlertCircle className=\"size-12 mx-auto text-status-warning-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.cannotChangePlan', 'Cannot Change Plan')}\n </h2>\n <p className=\"text-sm text-text-secondary max-w-md mx-auto\">\n {tr(\n 'billing.subscriptions.notActiveMessage',\n 'This subscription is no longer active. You can only change plans for active subscriptions.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button\"\n >\n {tr('billing.subscriptions.viewAll', 'View All Subscriptions')}\n </button>\n </div>\n </div>\n );\n }\n\n // No other plans available\n if (availablePlans.length === 0) {\n return (\n <div className=\"max-w-2xl mx-auto p-6 space-y-6\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <div className=\"text-center space-y-4 py-12\">\n <Package className=\"size-12 mx-auto text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.noPlansAvailable', 'No Other Plans Available')}\n </h2>\n <p className=\"text-sm text-text-secondary max-w-md mx-auto\">\n {tr(\n 'billing.subscriptions.noPlansMessage',\n 'There are no other active plans available for this product.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const currentPlanFeatures = (currentPlanWithFeatures?.features || []) as PlanFeature[];\n\n return (\n <div className=\"max-w-3xl mx-auto p-6 space-y-6\">\n {/* Header */}\n <div>\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary mb-4\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.subscriptions.changeYourPlan', 'Change Your Plan')}\n </h1>\n <p className=\"text-text-secondary mt-1\">\n Switch from <span className=\"font-medium\">{subscription.plan?.name}</span> to a different\n plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.subscriptions.upgradePurpose',\n 'Move this subscription to a different plan — upgrade for more capacity or downgrade to save. Compare what each plan includes and see how the change affects your billing before you confirm.'\n )}\n </PagePurpose>\n </div>\n\n {/* Current Plan Card */}\n <div className=\"bg-bg-sunken border border-border-subtle rounded-lg p-4\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex-1\">\n <p className=\"text-sm text-text-secondary\">Current Plan</p>\n <p className=\"font-semibold text-text-primary\">{subscription.plan?.name}</p>\n\n {/* Current Plan Features - Collapsible */}\n <CollapsibleFeatures\n features={currentPlanFeatures}\n isExpanded={expandedFeatures['current'] || false}\n onToggle={() => toggleFeatures('current')}\n variant=\"current\"\n />\n </div>\n <div className=\"text-right shrink-0 ml-4\">\n <p className=\"text-sm text-text-secondary\">Price</p>\n <p className=\"font-semibold text-text-primary\">\n {formatCurrency(subscription.plan?.price || 0, subscription.plan?.currency || 'USD')}\n <span className=\"text-sm font-normal text-text-secondary\">\n {' '}\n / {formatPlanDuration((subscription.plan?.duration || 'monthly') as PlanDuration)}\n </span>\n </p>\n </div>\n </div>\n </div>\n\n {/* Available Plans */}\n <div className=\"space-y-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Select New Plan</h2>\n <div className=\"grid gap-3\">\n {availablePlans.map((plan) => {\n const priceDiff = plan.price - (subscription.plan?.price || 0);\n const isSelected = selectedPlanId === plan.id;\n const planFeatures = (plan.features || []) as PlanFeature[];\n const isFeaturesExpanded = expandedFeatures[plan.id] || false;\n\n return (\n <div\n key={plan.id}\n className={`rounded-lg border transition-all ${\n isSelected\n ? 'border-border-strong bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-focus-ring)]'\n : 'border-border-subtle bg-bg-surface hover:border-border-strong'\n }`}\n >\n {/* Plan Header - Clickable for selection */}\n <button\n type=\"button\"\n onClick={() => setSelectedPlanId(plan.id)}\n className=\"w-full p-4 text-left\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-3\">\n <div\n className={`size-5 mt-0.5 rounded-full border-2 flex items-center justify-center shrink-0 ${\n isSelected\n ? 'border-border-strong bg-action-primary-bg'\n : 'border-border-default'\n }`}\n >\n {isSelected && <CheckCircle className=\"size-3 text-action-primary-text\" />}\n </div>\n <div className=\"flex-1\">\n <p className=\"font-semibold text-text-primary\">{plan.name}</p>\n {priceDiff !== 0 && (\n <p\n className={`text-sm ${priceDiff > 0 ? 'text-status-warning-text' : 'text-status-success-text'}`}\n >\n {priceDiff > 0 ? '+' : ''}\n {formatCurrency(priceDiff, plan.currency)} /{' '}\n {formatPlanDuration(plan.duration)}\n </p>\n )}\n </div>\n </div>\n <div className=\"text-right shrink-0 ml-4\">\n <p className=\"font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </p>\n <p className=\"text-sm text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n </div>\n </button>\n\n {/* Features Section - Below the main content */}\n <div className=\"px-4 pb-4 pt-0 ml-8\">\n <CollapsibleFeatures\n features={planFeatures}\n isExpanded={isFeaturesExpanded}\n onToggle={() => toggleFeatures(plan.id)}\n variant=\"available\"\n />\n </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Summary & Action */}\n {selectedPlan &&\n (() => {\n const currentAggregatedCount = aggregateFeaturesByQuota(currentPlanFeatures).length;\n const selectedPlanFeatures = (selectedPlan.features || []) as PlanFeature[];\n const selectedAggregatedCount = aggregateFeaturesByQuota(selectedPlanFeatures).length;\n\n return (\n <div className=\"bg-bg-surface border border-border-subtle rounded-lg p-4 space-y-4\">\n {/* Plan change visualization */}\n <div className=\"flex items-center justify-center gap-4 py-2\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">From</p>\n <p className=\"font-medium text-text-primary\">{subscription.plan?.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {currentAggregatedCount} feature{currentAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n <ArrowRight className=\"size-5 text-text-secondary\" />\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">To</p>\n <p className=\"font-medium text-text-link\">{selectedPlan.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {selectedAggregatedCount} feature{selectedAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n\n {/* Info message */}\n <p className=\"text-sm text-text-secondary text-center\">\n You'll be taken to checkout to select payment method and complete the change.\n </p>\n\n {/* Action Buttons */}\n <div className=\"flex items-center justify-end gap-3 pt-2 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(\n withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId)\n )\n }\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleProceedToCheckout}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n {isDowngrade ? 'Downgrade Plan' : 'Upgrade Plan'}\n </button>\n </div>\n </div>\n );\n })()}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UpgradeSubscriptionPage.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.tsx"],"sourcesContent":["/**\n * Change Plan Page\n * Allows users to change their current subscription to a different plan\n * Shows plan features in collapsible sections for easy comparison\n */\n\nimport { type FC, useState, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport {\n ArrowLeft,\n ArrowRight,\n Loader2,\n Package,\n CheckCircle,\n AlertCircle,\n ChevronDown,\n ChevronUp,\n Sparkles,\n Infinity as InfinityIcon,\n} from 'lucide-react';\n\nimport { useSubscription } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { formatCurrency, formatPlanDuration } from '../../../shared/utils/format';\nimport {\n useGetAllPlansQuery,\n useGetDefaultBillingAccountDashboardQuery,\n} from '../../../../generated/global-operations';\nimport type { Plan, PlanDuration, SubscriptionStatus } from '../../../shared/types';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\n\n// Helper type for plan features\ninterface PlanFeature {\n id: string;\n quotaId?: string;\n quota?: {\n id: string;\n name: string;\n limits?: Record<string, number | string | boolean>;\n reusable?: boolean;\n quotaType?: string;\n };\n}\n\n// Helper function to format limit values nicely\nconst formatLimitValue = (value: number | string | boolean): string => {\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (typeof value === 'number') {\n if (value === -1 || value === Infinity) return 'Unlimited';\n if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;\n if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;\n return value.toLocaleString();\n }\n return String(value);\n};\n\n// Helper function to get a user-friendly limit key name\nconst formatLimitKey = (key: string): string => {\n return key\n .replace(/_/g, ' ')\n .replace(/([A-Z])/g, ' $1')\n .replace(/^./, (str) => str.toUpperCase())\n .trim();\n};\n\n// Helper function to aggregate features by quotaId\n// Combines limits for features with the same quota\nconst aggregateFeaturesByQuota = (features: PlanFeature[]): PlanFeature[] => {\n const quotaMap = new Map<string, PlanFeature>();\n\n for (const feature of features) {\n const quotaId = feature.quotaId || feature.quota?.id;\n if (!quotaId || !feature.quota) continue;\n\n const existing = quotaMap.get(quotaId);\n if (existing && existing.quota) {\n // Aggregate limits - sum numeric values\n const existingLimits = existing.quota.limits || {};\n const newLimits = feature.quota.limits || {};\n const combinedLimits: Record<string, number | string | boolean> = { ...existingLimits };\n\n for (const [key, value] of Object.entries(newLimits)) {\n if (typeof value === 'number' && typeof combinedLimits[key] === 'number') {\n // Sum numeric values (but keep -1/unlimited as unlimited)\n const existingVal = combinedLimits[key] as number;\n if (existingVal === -1 || value === -1) {\n combinedLimits[key] = -1; // Unlimited stays unlimited\n } else {\n combinedLimits[key] = existingVal + value;\n }\n } else if (!(key in combinedLimits)) {\n combinedLimits[key] = value;\n }\n }\n\n existing.quota = {\n ...existing.quota,\n limits: combinedLimits,\n };\n } else {\n // Clone the feature to avoid mutating original\n quotaMap.set(quotaId, {\n ...feature,\n quota: feature.quota\n ? {\n ...feature.quota,\n limits: feature.quota.limits ? { ...feature.quota.limits } : undefined,\n }\n : undefined,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n};\n\n// Collapsible Features Component\ninterface CollapsibleFeaturesProps {\n features: PlanFeature[];\n isExpanded: boolean;\n onToggle: () => void;\n variant?: 'current' | 'available';\n}\n\nconst CollapsibleFeatures: FC<CollapsibleFeaturesProps> = ({\n features,\n isExpanded,\n onToggle,\n variant = 'available',\n}) => {\n // Aggregate features with same quotaId\n const aggregatedFeatures = useMemo(() => aggregateFeaturesByQuota(features), [features]);\n const featureCount = aggregatedFeatures.length;\n\n if (featureCount === 0) {\n return <p className=\"text-xs text-text-secondary italic\">No features defined</p>;\n }\n\n return (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggle();\n }}\n className={`flex items-center gap-1.5 text-xs font-medium transition-colors ${\n variant === 'current'\n ? 'text-text-secondary hover:text-text-primary'\n : 'text-text-link hover:text-text-link'\n }`}\n >\n <Sparkles className=\"size-3\" />\n <span>\n {featureCount} feature{featureCount !== 1 ? 's' : ''}\n </span>\n {isExpanded ? <ChevronUp className=\"size-3\" /> : <ChevronDown className=\"size-3\" />}\n </button>\n\n {isExpanded && (\n <div className={`mt-2 space-y-2 ${variant === 'current' ? 'pl-0' : 'pl-1'}`}>\n {aggregatedFeatures.map((feature) => {\n const quota = feature.quota;\n if (!quota) return null;\n\n const limits = quota.limits || {};\n const limitEntries = Object.entries(limits);\n\n return (\n <div\n key={feature.id}\n className={`rounded-button p-2 ${\n variant === 'current' ? 'bg-bg-sunken' : 'bg-[var(--color-accent-soft)]'\n }`}\n >\n <p\n className={`text-xs font-medium ${\n variant === 'current' ? 'text-text-primary' : 'text-text-primary'\n }`}\n >\n {quota.name}\n </p>\n {limitEntries.length > 0 && (\n <div className=\"mt-1 flex flex-wrap gap-x-3 gap-y-1\">\n {limitEntries.map(([key, value]) => (\n <div\n key={key}\n className=\"flex items-center gap-1 text-xs text-text-secondary\"\n >\n <span className=\"opacity-70\">{formatLimitKey(key)}:</span>\n <span className=\"font-medium text-text-primary flex items-center gap-0.5\">\n {value === -1 || value === Infinity ? (\n <>\n <InfinityIcon className=\"size-3\" />\n <span className=\"sr-only\">Unlimited</span>\n </>\n ) : (\n formatLimitValue(value as number | string | boolean)\n )}\n </span>\n </div>\n ))}\n </div>\n )}\n {quota.reusable && (\n <span className=\"inline-block mt-1 text-[10px] px-1.5 py-0.5 rounded bg-status-success-bg-subtle text-status-success-text\">\n Reusable\n </span>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n\nexport const UpgradeSubscriptionPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { subscriptionId } = useParams<{ subscriptionId: string }>();\n const navigateTo = useBillingNavigate();\n const { orgId } = useBilling();\n const permissions = useBillingPermissions();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n\n const { data: defaultBillingAccountData } = useGetDefaultBillingAccountDashboardQuery({\n fetchPolicy: 'cache-first',\n });\n const billingAccountId =\n selectedAccountId ?? defaultBillingAccountData?.getDefaultBillingAccount?.id ?? undefined;\n const { subscription, isLoading: subscriptionLoading } = useSubscription(\n subscriptionId,\n billingAccountId\n );\n\n // Fetch available plans (includes features)\n const { data: plansData, loading: plansLoading } = useGetAllPlansQuery();\n\n // State\n const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);\n const [expandedFeatures, setExpandedFeatures] = useState<Record<string, boolean>>({});\n\n // Get all plans including current for feature lookup\n const allPlans = useMemo(() => {\n return (plansData?.getAllPlans || []) as Plan[];\n }, [plansData]);\n\n // Get current plan with features from allPlans\n const currentPlanWithFeatures = useMemo(() => {\n const currentPlanId = subscription?.planId;\n return allPlans.find((p) => p.id === currentPlanId) || null;\n }, [allPlans, subscription?.planId]);\n\n // Filter plans - show all other active plans (same product if productID exists, otherwise all)\n const availablePlans = useMemo(() => {\n const currentPlanId = subscription?.planId;\n const currentProductId = subscription?.plan?.productID;\n\n return allPlans\n .filter((plan) => {\n // Always exclude current plan\n if (plan.id === currentPlanId) return false;\n // Only show active plans\n if (!plan.isActive) return false;\n // If we have productID, filter by same product\n if (currentProductId) {\n return plan.productID === currentProductId;\n }\n // If no productID, show all active plans\n return true;\n })\n .sort((a, b) => a.price - b.price);\n }, [allPlans, subscription]);\n\n const selectedPlan = useMemo(() => {\n return availablePlans.find((p) => p.id === selectedPlanId) || null;\n }, [availablePlans, selectedPlanId]);\n\n const isDowngrade =\n selectedPlan !== null && selectedPlan.price < (subscription?.plan?.price ?? 0);\n\n // Toggle feature expansion\n const toggleFeatures = (planId: string) => {\n setExpandedFeatures((prev) => ({\n ...prev,\n [planId]: !prev[planId],\n }));\n };\n\n // Handle proceed to checkout\n const handleProceedToCheckout = () => {\n if (!subscription || !selectedPlan) return;\n const currentPrice = subscription.plan?.price ?? 0;\n const isDowngrade = selectedPlan.price < currentPrice;\n navigateTo(`/checkout/${selectedPlan.id}`, {\n state: {\n purchaseType: 'PLAN',\n // Upgrade: old quotas transfer to new plan immediately\n // Downgrade: old plan runs to natural expiry, auto-renewal cancelled\n ...(isDowngrade\n ? { downgradeFromSubscriptionId: subscription.id }\n : { upgradeFromSubscriptionId: subscription.id }),\n currentPlanName: subscription.plan?.name,\n billingAccountId: subscription.billingAccountId,\n },\n });\n };\n\n // Permission check\n if (!permissions.canUpdateSubscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-bg-sunken flex items-center justify-center\">\n <AlertCircle className=\"size-6 text-text-secondary\" />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">\n You don't have permission to change subscriptions.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (subscriptionLoading || plansLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <Loader2 className=\"size-8 animate-spin mx-auto text-text-link\" />\n <p className=\"text-text-secondary\">Loading plans…</p>\n </div>\n </div>\n );\n }\n\n // Subscription not found\n if (!subscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <AlertCircle className=\"size-12 mx-auto text-status-error-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">Subscription not found</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button\"\n >\n {tr('billing.subscriptions.backToSubscriptions', 'Back to Subscriptions')}\n </button>\n </div>\n </div>\n );\n }\n\n // Subscription is not active (already upgraded/canceled/etc)\n const isActive = subscription.status === ('active' as SubscriptionStatus);\n if (!isActive) {\n return (\n <div className=\"max-w-2xl mx-auto p-6 space-y-6\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <div className=\"text-center space-y-4 py-12\">\n <AlertCircle className=\"size-12 mx-auto text-status-warning-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.cannotChangePlan', 'Cannot Change Plan')}\n </h2>\n <p className=\"text-sm text-text-secondary max-w-md mx-auto\">\n {tr(\n 'billing.subscriptions.notActiveMessage',\n 'This subscription is no longer active. You can only change plans for active subscriptions.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button\"\n >\n {tr('billing.subscriptions.viewAll', 'View All Subscriptions')}\n </button>\n </div>\n </div>\n );\n }\n\n // No other plans available\n if (availablePlans.length === 0) {\n return (\n <div className=\"max-w-2xl mx-auto p-6 space-y-6\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <div className=\"text-center space-y-4 py-12\">\n <Package className=\"size-12 mx-auto text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.noPlansAvailable', 'No Other Plans Available')}\n </h2>\n <p className=\"text-sm text-text-secondary max-w-md mx-auto\">\n {tr(\n 'billing.subscriptions.noPlansMessage',\n 'There are no other active plans available for this product.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const currentPlanFeatures = (currentPlanWithFeatures?.features || []) as PlanFeature[];\n\n return (\n <div className=\"max-w-3xl mx-auto p-6 space-y-6\">\n {/* Header */}\n <div>\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary mb-4\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.subscriptions.changeYourPlan', 'Change Your Plan')}\n </h1>\n <p className=\"text-text-secondary mt-1\">\n Switch from <span className=\"font-medium\">{subscription.plan?.name}</span> to a different\n plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.subscriptions.upgradePurpose',\n 'Move this subscription to a different plan — upgrade for more capacity or downgrade to save. Compare what each plan includes and see how the change affects your billing before you confirm.'\n )}\n </PagePurpose>\n </div>\n\n {/* Current Plan Card */}\n <div className=\"bg-bg-sunken border border-border-subtle rounded-lg p-4\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex-1\">\n <p className=\"text-sm text-text-secondary\">Current Plan</p>\n <p className=\"font-semibold text-text-primary\">{subscription.plan?.name}</p>\n\n {/* Current Plan Features - Collapsible */}\n <CollapsibleFeatures\n features={currentPlanFeatures}\n isExpanded={expandedFeatures['current'] || false}\n onToggle={() => toggleFeatures('current')}\n variant=\"current\"\n />\n </div>\n <div className=\"text-right shrink-0 ml-4\">\n <p className=\"text-sm text-text-secondary\">Price</p>\n <p className=\"font-semibold text-text-primary\">\n {formatCurrency(subscription.plan?.price || 0, subscription.plan?.currency || 'USD')}\n <span className=\"text-sm font-normal text-text-secondary\">\n {' '}\n / {formatPlanDuration((subscription.plan?.duration || 'monthly') as PlanDuration)}\n </span>\n </p>\n </div>\n </div>\n </div>\n\n {/* Available Plans */}\n <div className=\"space-y-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Select New Plan</h2>\n <div className=\"grid gap-3\">\n {availablePlans.map((plan) => {\n const priceDiff = plan.price - (subscription.plan?.price || 0);\n const isSelected = selectedPlanId === plan.id;\n const planFeatures = (plan.features || []) as PlanFeature[];\n const isFeaturesExpanded = expandedFeatures[plan.id] || false;\n\n return (\n <div\n key={plan.id}\n className={`rounded-lg border transition-all ${\n isSelected\n ? 'border-border-strong bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-focus-ring)]'\n : 'border-border-subtle bg-bg-surface hover:border-border-strong'\n }`}\n >\n {/* Plan Header - Clickable for selection */}\n <button\n type=\"button\"\n onClick={() => setSelectedPlanId(plan.id)}\n className=\"w-full p-4 text-left\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-3\">\n <div\n className={`size-5 mt-0.5 rounded-full border-2 flex items-center justify-center shrink-0 ${\n isSelected\n ? 'border-border-strong bg-action-primary-bg'\n : 'border-border-default'\n }`}\n >\n {isSelected && <CheckCircle className=\"size-3 text-action-primary-text\" />}\n </div>\n <div className=\"flex-1\">\n <p className=\"font-semibold text-text-primary\">{plan.name}</p>\n {priceDiff !== 0 && (\n <p\n className={`text-sm ${priceDiff > 0 ? 'text-status-warning-text' : 'text-status-success-text'}`}\n >\n {priceDiff > 0 ? '+' : ''}\n {formatCurrency(priceDiff, plan.currency)} /{' '}\n {formatPlanDuration(plan.duration)}\n </p>\n )}\n </div>\n </div>\n <div className=\"text-right shrink-0 ml-4\">\n <p className=\"font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </p>\n <p className=\"text-sm text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n </div>\n </button>\n\n {/* Features Section - Below the main content */}\n <div className=\"px-4 pb-4 pt-0 ml-8\">\n <CollapsibleFeatures\n features={planFeatures}\n isExpanded={isFeaturesExpanded}\n onToggle={() => toggleFeatures(plan.id)}\n variant=\"available\"\n />\n </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Summary & Action */}\n {selectedPlan &&\n (() => {\n const currentAggregatedCount = aggregateFeaturesByQuota(currentPlanFeatures).length;\n const selectedPlanFeatures = (selectedPlan.features || []) as PlanFeature[];\n const selectedAggregatedCount = aggregateFeaturesByQuota(selectedPlanFeatures).length;\n\n return (\n <div className=\"bg-bg-surface border border-border-subtle rounded-lg p-4 space-y-4\">\n {/* Plan change visualization */}\n <div className=\"flex items-center justify-center gap-4 py-2\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">From</p>\n <p className=\"font-medium text-text-primary\">{subscription.plan?.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {currentAggregatedCount} feature{currentAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n <ArrowRight className=\"size-5 text-text-secondary\" />\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">To</p>\n <p className=\"font-medium text-text-link\">{selectedPlan.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {selectedAggregatedCount} feature{selectedAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n\n {/* Info message */}\n <p className=\"text-sm text-text-secondary text-center\">\n You'll be taken to checkout to select payment method and complete the change.\n </p>\n\n {/* Action Buttons */}\n <div className=\"flex items-center justify-end gap-3 pt-2 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(\n withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId)\n )\n }\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleProceedToCheckout}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n {isDowngrade ? 'Downgrade Plan' : 'Upgrade Plan'}\n </button>\n </div>\n </div>\n );\n })()}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useUsage.js","names":[],"sources":["../../../../../src/billing/modules/usage/hooks/useUsage.ts"],"sourcesContent":["/**\n * Usage Module - Hooks\n * Custom hooks for usage data fetching and mutations\n */\n\nimport { useMemo, useCallback, useState, useEffect } from 'react';\nimport {\n useGetUsageDashboardQuery,\n useGetBillingAccountQuotasSummaryForUsageQuery,\n useGetSubscriptionsWithQuotasQuery,\n useGetPastSubscriptionsWithQuotasQuery,\n useGetBillingAccountsForUsageQuery,\n useGetQuotaAnalyticsForUsageQuery,\n type GetPastSubscriptionsWithQuotasQuery,\n} from '../../../../generated/global-operations';\nimport {\n useGetUsageRecordsQuery,\n type GetUsageRecordsQuery,\n type GetUsageRecordsQueryVariables,\n} from '../../../../generated/wspace-operations';\nimport { useGatewayClient, useQuery, useMutation, gql } from '@burdenoff/fe-libs/shared/graphql';\nimport { AnalyticsPeriod, QuotaStatus } from '../../../shared/types';\nimport type { UsageDashboard } from '../../../shared/types';\n\n// ============================================================================\n// Workspace Quota Assignment — managed by activity-svc\n// ============================================================================\n\nconst WORKSPACE_QUOTA_OVERVIEW_QUERY = gql`\n query WorkspaceQuotaOverview($workspaceId: ID!) {\n workspaceQuotaOverview(workspaceId: $workspaceId) {\n assignment {\n id\n workspaceId\n quotaName\n productId\n quotaMode\n billingAccountId\n subscriptionId\n limit\n used\n noLimit\n resetPeriod\n lastResetAt\n isActive\n createdAt\n }\n liveStatus {\n workspaceId\n quotaName\n productId\n currentValue\n limit\n noLimit\n scrapedAt\n }\n effectiveUsed\n remaining\n usagePercentage\n }\n }\n`;\n\nconst UNASSIGN_WORKSPACE_QUOTA_MUTATION = gql`\n mutation UnassignWorkspaceQuota($id: ID!, $workspaceId: ID!) {\n unassignWorkspaceQuota(id: $id, workspaceId: $workspaceId)\n }\n`;\n\nexport interface WorkspaceQuotaAssignmentItem {\n id: string;\n workspaceId: string;\n quotaName: string;\n productId: string;\n quotaMode: 'dedicated' | 'pooled';\n billingAccountId: string | null;\n subscriptionId: string | null;\n limit: number | null;\n used: number;\n noLimit: boolean;\n resetPeriod: string;\n lastResetAt: string | null;\n isActive: boolean;\n createdAt: string;\n}\n\nexport interface WorkspaceQuotaOverviewItem {\n assignment: WorkspaceQuotaAssignmentItem;\n liveStatus: WorkspaceQuotaStatusItem | null;\n effectiveUsed: number;\n remaining: number | null;\n usagePercentage: number;\n}\n\nexport interface AssignWorkspaceQuotaInput {\n workspaceId: string;\n quotaName: string;\n productId: string;\n quotaMode?: 'dedicated' | 'pooled';\n billingAccountId?: string;\n subscriptionId?: string;\n limit?: number;\n noLimit?: boolean;\n resetPeriod?: string;\n metadata?: Record<string, unknown>;\n}\n\ninterface UseWorkspaceQuotaOverviewResult {\n overview: WorkspaceQuotaOverviewItem[];\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useWorkspaceQuotaOverview(workspaceId?: string): UseWorkspaceQuotaOverviewResult {\n const wsClient = useGatewayClient('workspace');\n const { data, loading, error, refetch } = useQuery<{\n workspaceQuotaOverview: WorkspaceQuotaOverviewItem[];\n }>(WORKSPACE_QUOTA_OVERVIEW_QUERY, {\n client: wsClient,\n variables: { workspaceId },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n overview: data?.workspaceQuotaOverview ?? [],\n isLoading: loading,\n error: error ?? null,\n refetch,\n };\n}\n\n// ============================================================================\n// activateSubscriptionForWorkspace — single backend call that assigns all\n// WORKSPACE-scoped quotas from the purchased plan. Replaces the N individual\n// assignWorkspaceQuota calls so the UI never sends quota names/limits to the server.\n// ============================================================================\n\nconst ACTIVATE_SUBSCRIPTION_FOR_WORKSPACE_MUTATION = gql`\n mutation ActivateSubscriptionForWorkspace(\n $subscriptionId: ID!\n $billingAccountId: ID!\n $workspaceId: ID!\n ) {\n activateSubscriptionForWorkspace(\n subscriptionId: $subscriptionId\n billingAccountId: $billingAccountId\n workspaceId: $workspaceId\n ) {\n workspaceId\n assignedQuotas\n skippedQuotas\n }\n }\n`;\n\nexport interface ActivateSubscriptionForWorkspaceResult {\n workspaceId: string;\n assignedQuotas: number;\n skippedQuotas: number;\n}\n\ninterface UseActivateSubscriptionForWorkspaceResult {\n activate: (\n subscriptionId: string,\n billingAccountId: string,\n workspaceId: string\n ) => Promise<ActivateSubscriptionForWorkspaceResult | null>;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useActivateSubscriptionForWorkspace(): UseActivateSubscriptionForWorkspaceResult {\n const globalClient = useGatewayClient('global');\n const [mutate, { loading, error }] = useMutation<\n { activateSubscriptionForWorkspace: ActivateSubscriptionForWorkspaceResult },\n { subscriptionId: string; billingAccountId: string; workspaceId: string }\n >(ACTIVATE_SUBSCRIPTION_FOR_WORKSPACE_MUTATION, { client: globalClient });\n\n const activate = async (\n subscriptionId: string,\n billingAccountId: string,\n workspaceId: string\n ) => {\n const result = await mutate({ variables: { subscriptionId, billingAccountId, workspaceId } });\n return result.data?.activateSubscriptionForWorkspace ?? null;\n };\n\n return { activate, isLoading: loading, error: error ?? null };\n}\n\ninterface UseUnassignWorkspaceQuotaResult {\n unassign: (id: string, workspaceId: string) => Promise<boolean>;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useUnassignWorkspaceQuota(): UseUnassignWorkspaceQuotaResult {\n const wsClient = useGatewayClient('workspace');\n const [mutate, { loading, error }] = useMutation<\n { unassignWorkspaceQuota: boolean },\n { id: string; workspaceId: string }\n >(UNASSIGN_WORKSPACE_QUOTA_MUTATION, { client: wsClient });\n\n const unassign = async (id: string, workspaceId: string) => {\n const result = await mutate({ variables: { id, workspaceId } });\n return result.data?.unassignWorkspaceQuota ?? false;\n };\n\n return { unassign, isLoading: loading, error: error ?? null };\n}\n\n// ============================================================================\n// Workspace Quota Status — live values from activity-svc\n// ============================================================================\n\nconst WORKSPACE_QUOTA_STATUSES_QUERY = gql`\n query WorkspaceQuotaStatuses($workspaceId: ID!) {\n workspaceQuotaStatuses(workspaceId: $workspaceId) {\n workspaceId\n quotaName\n productId\n currentValue\n limit\n noLimit\n scrapedAt\n }\n }\n`;\n\nconst WORKSPACE_QUOTA_HISTORY_QUERY = gql`\n query WorkspaceQuotaHistory($workspaceId: ID!, $quotaName: String!, $limit: Int) {\n workspaceQuotaHistory(workspaceId: $workspaceId, quotaName: $quotaName, limit: $limit) {\n id\n workspaceId\n quotaName\n productId\n value\n limit\n noLimit\n snapshotAt\n }\n }\n`;\n\nexport interface WorkspaceQuotaStatusItem {\n workspaceId: string;\n quotaName: string;\n productId: string;\n currentValue: number;\n limit: number | null;\n noLimit: boolean;\n scrapedAt: string;\n}\n\nexport interface WorkspaceQuotaSnapshotItem {\n id: string;\n workspaceId: string;\n quotaName: string;\n productId: string;\n value: number;\n limit: number | null;\n noLimit: boolean;\n snapshotAt: string;\n}\n\ninterface UseWorkspaceQuotaStatusesResult {\n statuses: WorkspaceQuotaStatusItem[];\n statusMap: Map<string, WorkspaceQuotaStatusItem>;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useWorkspaceQuotaStatuses(workspaceId?: string): UseWorkspaceQuotaStatusesResult {\n const wsClient = useGatewayClient('workspace');\n const { data, loading, error } = useQuery<{ workspaceQuotaStatuses: WorkspaceQuotaStatusItem[] }>(\n WORKSPACE_QUOTA_STATUSES_QUERY,\n {\n client: wsClient,\n variables: { workspaceId },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n }\n );\n\n const { statuses, statusMap } = useMemo(() => {\n const list = data?.workspaceQuotaStatuses ?? [];\n return { statuses: list, statusMap: new Map(list.map((s) => [s.quotaName, s])) };\n }, [data]);\n\n return { statuses, statusMap, isLoading: loading, error: error ?? null };\n}\n\ninterface UseWorkspaceQuotaHistoryResult {\n snapshots: WorkspaceQuotaSnapshotItem[];\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useWorkspaceQuotaHistory(\n workspaceId?: string,\n quotaName?: string,\n limit: number = 48\n): UseWorkspaceQuotaHistoryResult {\n const wsClient = useGatewayClient('workspace');\n const { data, loading, error } = useQuery<{\n workspaceQuotaHistory: WorkspaceQuotaSnapshotItem[];\n }>(WORKSPACE_QUOTA_HISTORY_QUERY, {\n client: wsClient,\n variables: { workspaceId, quotaName, limit },\n skip: !workspaceId || !quotaName,\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n snapshots: data?.workspaceQuotaHistory ?? [],\n isLoading: loading,\n error: error ?? null,\n };\n}\n\n// ============================================================================\n// Dashboard Hook\n// ============================================================================\n\ninterface UseUsageDashboardParams {\n billingAccountId?: string;\n period?: AnalyticsPeriod;\n}\n\ninterface UseUsageDashboardResult {\n dashboard: UsageDashboard | null;\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useUsageDashboard({\n billingAccountId,\n period = AnalyticsPeriod.LAST_30_DAYS,\n}: UseUsageDashboardParams): UseUsageDashboardResult {\n const { data, loading, error, refetch } = useGetUsageDashboardQuery({\n variables: { billingAccountId: billingAccountId!, period },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n dashboard: data?.getUsageDashboard ?? null,\n isLoading: loading,\n error: error ?? null,\n refetch,\n };\n}\n\n// ============================================================================\n// Enhanced Usage Page Hooks\n// ============================================================================\n\n/**\n * Quota assignment from the API\n */\ninterface QuotaAssignmentSummary {\n id: string;\n name: string;\n unit: string;\n quotaType: 'POOLED' | 'PRODUCT_SPECIFIC';\n reusable: boolean;\n limit: number;\n limitLeft: number;\n used: number;\n usagePercentage: number;\n noLimit: boolean;\n subscriptionId?: string;\n context?: Record<string, unknown> | null;\n recentUsages: Array<{\n id: string;\n amount: number;\n description: string;\n timestamp: string;\n tags?: Record<string, unknown>;\n }>;\n createdAt: string;\n endtime: string;\n}\n\n/**\n * Aggregated quota for display (multiple quota assignments with same name combined)\n */\nexport interface AggregatedQuota {\n name: string;\n displayName?: string;\n description?: string;\n unit: string;\n totalLimit: number;\n totalUsed: number;\n usagePercentage: number;\n status: QuotaStatus;\n quotaCount: number;\n noLimit: boolean;\n reusable: boolean;\n /** True when usage comes from the activity-svc scraper, not billing recordUsage events. */\n isCollectorBased: boolean;\n quotaAssignments: QuotaAssignmentSummary[];\n}\n\n/**\n * Subscription with aggregated quotas\n */\nexport interface SubscriptionWithAggregatedQuotas {\n id: string;\n status: string;\n planName?: string;\n productId?: string;\n createdAt: string;\n aggregatedQuotas: AggregatedQuota[];\n}\n\n/**\n * Helper function to calculate quota status based on usage percentage\n */\nfunction calculateQuotaStatus(\n usagePercentage: number,\n limitLeft: number,\n noLimit = false\n): QuotaStatus {\n if (noLimit) return QuotaStatus.HEALTHY;\n if (limitLeft <= 0) return QuotaStatus.EXHAUSTED;\n if (usagePercentage >= 90) return QuotaStatus.CRITICAL;\n if (usagePercentage >= 75) return QuotaStatus.WARNING;\n return QuotaStatus.HEALTHY;\n}\n\n// Maps billing quota names to their corresponding scraper metric names in workspace_quota_statuses.\n// Scraper keys use dot-notation product prefix; billing keys use kebab-case.\nconst BILLING_TO_SCRAPER_METRIC: Record<string, string> = {\n 'agents-registered': 'vibecontrols.agents.count',\n 'vibes-created': 'vibecontrols.vibes.count',\n 'catalogs-created': 'vibecontrols.catalogs.count',\n 'tunnels-created': 'vibecontrols.tunnels.count',\n 'sessions-started': 'vibecontrols.sessions.starts',\n};\n\n/**\n * Merges live collector values from activity-svc into a list of aggregated quotas.\n * Overrides `totalUsed` (and derived fields) when a live value exists, and distributes\n * the live total across individual assignments (earliest-expiry first) so the\n * Quota Breakdown shows real per-assignment consumption instead of always-0.\n */\nfunction mergeLiveValues(\n aggregated: AggregatedQuota[],\n statusMap: Map<string, WorkspaceQuotaStatusItem>\n): AggregatedQuota[] {\n if (statusMap.size === 0) return aggregated;\n return aggregated.map((q) => {\n const scraperKey = BILLING_TO_SCRAPER_METRIC[q.name] ?? q.name;\n const live = statusMap.get(scraperKey);\n if (!live) return q;\n\n const totalUsed = live.currentValue;\n const { totalLimit, noLimit } = q;\n const usagePercentage = noLimit || totalLimit === 0 ? 0 : (totalUsed / totalLimit) * 100;\n const limitLeft = noLimit ? 0 : Math.max(0, totalLimit - totalUsed);\n\n // Distribute live total into assignments: fill earliest-expiry first, overflow to next.\n let remaining = totalUsed;\n const updatedAssignments = [...q.quotaAssignments]\n .sort((a, b) => {\n const dateA = a.endtime ? new Date(a.endtime).getTime() : Infinity;\n const dateB = b.endtime ? new Date(b.endtime).getTime() : Infinity;\n return dateA - dateB;\n })\n .map((a) => {\n const assignmentUsed = Math.min(remaining, a.limit || 0);\n remaining = Math.max(0, remaining - assignmentUsed);\n return { ...a, used: assignmentUsed };\n });\n\n return {\n ...q,\n totalUsed,\n usagePercentage,\n isCollectorBased: true,\n status: calculateQuotaStatus(usagePercentage, limitLeft, noLimit),\n quotaAssignments: updatedAssignments,\n };\n });\n}\n\n/**\n * Helper function to aggregate quotas by name\n */\nfunction aggregateQuotasByName(quotas: QuotaAssignmentSummary[]): AggregatedQuota[] {\n const grouped = new Map<string, QuotaAssignmentSummary[]>();\n\n for (const quota of quotas) {\n const existing = grouped.get(quota.name) || [];\n existing.push(quota);\n grouped.set(quota.name, existing);\n }\n\n const aggregated: AggregatedQuota[] = [];\n\n for (const [name, assignments] of grouped) {\n // Sum up limits and usage first\n const totalLimit = assignments.reduce((sum, q) => sum + (q.limit || 0), 0);\n const totalUsed = assignments.reduce((sum, q) => sum + (q.used || 0), 0);\n const totalLimitLeft = assignments.reduce((sum, q) => sum + (q.limitLeft || 0), 0);\n\n // Check if there's actually no limit (ignore noLimit flag if there's a numeric limit)\n // Only treat as unlimited if totalLimit is 0 or all assignments explicitly have noLimit AND no numeric limit\n const hasNoLimit =\n totalLimit === 0 && assignments.every((q) => q.noLimit || (q.limit || 0) === 0);\n\n // Calculate percentage\n const usagePercentage = hasNoLimit || totalLimit === 0 ? 0 : (totalUsed / totalLimit) * 100;\n\n // Get status\n const status = calculateQuotaStatus(usagePercentage, totalLimitLeft, hasNoLimit);\n\n // Get reusable, unit, and display info from first assignment\n const reusable = assignments[0]?.reusable || false;\n const unit = assignments[0]?.unit || '';\n const ctx = assignments[0]?.context;\n const displayName = typeof ctx?.displayName === 'string' ? ctx.displayName : undefined;\n const description = typeof ctx?.description === 'string' ? ctx.description : undefined;\n\n aggregated.push({\n name,\n displayName,\n description,\n unit,\n totalLimit: hasNoLimit ? 0 : totalLimit,\n totalUsed,\n usagePercentage,\n status,\n quotaCount: assignments.length,\n noLimit: hasNoLimit,\n isCollectorBased: false,\n reusable,\n quotaAssignments: assignments,\n });\n }\n\n // Sort by status priority (critical first) then by usage percentage\n const statusPriority: Record<QuotaStatus, number> = {\n EXHAUSTED: 0,\n CRITICAL: 1,\n WARNING: 2,\n HEALTHY: 3,\n INACTIVE: 4,\n };\n\n return aggregated.sort((a, b) => {\n const priorityDiff = statusPriority[a.status] - statusPriority[b.status];\n if (priorityDiff !== 0) return priorityDiff;\n return b.usagePercentage - a.usagePercentage;\n });\n}\n\n/**\n * Hook to get billing accounts for the selector\n */\ninterface UseBillingAccountsForUsageResult {\n billingAccounts: Array<{\n id: string;\n name: string;\n email: string;\n currency: string;\n creditAmount: number;\n isActive: boolean;\n isDefault: boolean;\n }>;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useBillingAccountsForUsage(): UseBillingAccountsForUsageResult {\n const { data, loading, error } = useGetBillingAccountsForUsageQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n billingAccounts: data?.getBillingAccountsByOrg ?? [],\n isLoading: loading,\n error: error ?? null,\n };\n}\n\n/**\n * Hook to get billing account quotas summary with aggregation\n * Only returns pooled quotas - subscription quotas come from useSubscriptionsWithQuotas\n */\ninterface UseBillingAccountQuotasSummaryResult {\n pooledQuotas: AggregatedQuota[];\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useBillingAccountQuotasSummary(\n billingAccountId: string | undefined,\n productId?: string\n): UseBillingAccountQuotasSummaryResult {\n const { data, loading, error, refetch } = useGetBillingAccountQuotasSummaryForUsageQuery({\n variables: { billingAccountId: billingAccountId!, productId },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const pooledQuotas = useMemo(() => {\n if (!data?.getBillingAccountQuotasSummary) {\n return [];\n }\n\n const summary = data.getBillingAccountQuotasSummary;\n return aggregateQuotasByName(summary.pooledQuotas || []);\n }, [data]);\n\n return {\n pooledQuotas,\n isLoading: loading,\n error: error ?? null,\n refetch,\n };\n}\n\n/**\n * Subscription from the API\n */\ninterface SubscriptionQuota {\n id: string;\n name: string;\n limits: { type: string; value: number; unit?: string; originalValue?: number };\n context?: Record<string, unknown> | null;\n currentUsageSum: number;\n usageCount: number;\n createdAt: string;\n endtime: string;\n}\n\ninterface SubscriptionFromAPI {\n id: string;\n status: string;\n createdAt: string;\n quotas: SubscriptionQuota[];\n}\n\n/**\n * Hook to get subscriptions with their quotas grouped by status\n * Uses lazy loading for past subscriptions and supports backend search\n */\ntype ViewMode = 'active' | 'past' | 'all';\n\nconst PAST_SUBSCRIPTIONS_PAGE_SIZE = 10;\n\ninterface UseSubscriptionsWithQuotasOptions {\n searchQuery?: string;\n viewMode?: ViewMode;\n}\n\ninterface UseSubscriptionsWithQuotasResult {\n activeSubscriptions: SubscriptionWithAggregatedQuotas[];\n pastSubscriptions: SubscriptionWithAggregatedQuotas[];\n isLoading: boolean;\n isPastSubscriptionsLoading: boolean;\n error: Error | null;\n refetch: () => void;\n // Pagination for past subscriptions\n loadMorePastSubscriptions: () => void;\n hasMorePastSubscriptions: boolean;\n isLoadingMorePast: boolean;\n pastSubscriptionsCount: number;\n}\n\nexport function useSubscriptionsWithQuotas(\n billingAccountId: string | undefined,\n options: UseSubscriptionsWithQuotasOptions = {}\n): UseSubscriptionsWithQuotasResult {\n const { searchQuery, viewMode = 'active' } = options;\n\n // Determine which subscriptions to fetch based on viewMode\n const shouldFetchActive = viewMode === 'active' || viewMode === 'all';\n const shouldFetchPast = viewMode === 'past' || viewMode === 'all';\n\n // Query for active subscriptions\n const { data, loading, error, refetch } = useGetSubscriptionsWithQuotasQuery({\n variables: {\n billingAccountId: billingAccountId!,\n searchQuery: searchQuery || null,\n },\n skip: !billingAccountId || !shouldFetchActive,\n fetchPolicy: 'cache-and-network',\n });\n\n // Query for past subscriptions with pagination - only when viewMode is 'past' or 'all'\n const {\n data: pastData,\n loading: pastLoading,\n refetch: refetchPast,\n fetchMore: fetchMorePast,\n } = useGetPastSubscriptionsWithQuotasQuery({\n variables: {\n billingAccountId: billingAccountId!,\n searchQuery: searchQuery || null,\n page: 1,\n pageSize: PAST_SUBSCRIPTIONS_PAGE_SIZE,\n },\n skip: !billingAccountId || !shouldFetchPast,\n fetchPolicy: 'cache-and-network',\n });\n\n // Track if we're loading more past subscriptions\n const [isLoadingMorePast, setIsLoadingMorePast] = useState(false);\n const [currentPastPage, setCurrentPastPage] = useState(1);\n\n // Helper function to process subscription data\n const processSubscription = useCallback(\n (sub: SubscriptionFromAPI): SubscriptionWithAggregatedQuotas => {\n const quotaSummaries: QuotaAssignmentSummary[] = (sub.quotas || []).map((q) => {\n const limit = q.limits?.value || 0;\n const used = q.currentUsageSum || 0;\n const limitLeft = Math.max(0, limit - used);\n const usagePercentage = limit > 0 ? (used / limit) * 100 : 0;\n const unit = q.limits?.unit || '';\n\n return {\n id: q.id,\n name: q.name,\n unit,\n quotaType: 'PRODUCT_SPECIFIC' as const,\n reusable: false,\n limit,\n limitLeft,\n used,\n usagePercentage,\n noLimit: q.limits?.type !== 'number' && q.limits?.type !== 'numeric',\n subscriptionId: sub.id,\n context: q.context,\n recentUsages: [],\n createdAt: q.createdAt,\n endtime: q.endtime,\n };\n });\n\n return {\n id: sub.id,\n status: sub.status,\n createdAt: sub.createdAt,\n aggregatedQuotas: aggregateQuotasByName(quotaSummaries),\n };\n },\n []\n );\n\n // Process active subscriptions\n const activeSubscriptions = useMemo(() => {\n if (!data?.getSubscriptionUsageOverview) {\n return [];\n }\n\n const subscriptions: SubscriptionFromAPI[] = data.getSubscriptionUsageOverview;\n return subscriptions.map(processSubscription);\n }, [data, processSubscription]);\n\n // Process past subscriptions\n const pastSubscriptions = useMemo(() => {\n if (!pastData?.getSubscriptionUsageOverview) {\n return [];\n }\n\n const subscriptions: SubscriptionFromAPI[] =\n pastData.getSubscriptionUsageOverview.subscriptions || pastData.getSubscriptionUsageOverview;\n return subscriptions.map(processSubscription);\n }, [pastData, processSubscription]);\n\n // Get pagination info for past subscriptions\n const pastSubscriptionsPagination = useMemo(() => {\n const overview = pastData?.getSubscriptionUsageOverview;\n if (!overview) {\n return { totalCount: 0, hasMore: false };\n }\n // If it's the new paginated format\n if (overview.totalCount !== undefined) {\n return {\n totalCount: overview.totalCount,\n hasMore: pastSubscriptions.length < overview.totalCount,\n };\n }\n // Legacy format - assume there might be more if we got a full page\n return {\n totalCount: pastSubscriptions.length,\n hasMore: pastSubscriptions.length >= PAST_SUBSCRIPTIONS_PAGE_SIZE * currentPastPage,\n };\n }, [pastData, pastSubscriptions.length, currentPastPage]);\n\n // Reset page when search query or viewMode changes\n useEffect(() => {\n setCurrentPastPage(1);\n }, [searchQuery, viewMode]);\n\n // Load more past subscriptions\n const loadMorePastSubscriptions = useCallback(async () => {\n if (!fetchMorePast || isLoadingMorePast || !pastSubscriptionsPagination.hasMore) return;\n\n setIsLoadingMorePast(true);\n const nextPage = currentPastPage + 1;\n\n try {\n await fetchMorePast({\n variables: {\n page: nextPage,\n pageSize: PAST_SUBSCRIPTIONS_PAGE_SIZE,\n },\n updateQuery: (\n prev: GetPastSubscriptionsWithQuotasQuery,\n { fetchMoreResult }: { fetchMoreResult: GetPastSubscriptionsWithQuotasQuery }\n ) => {\n if (!fetchMoreResult) return prev;\n\n const prevSubs = prev?.getSubscriptionUsageOverview ?? [];\n const newSubs = fetchMoreResult?.getSubscriptionUsageOverview ?? [];\n\n return {\n getSubscriptionUsageOverview: [...prevSubs, ...newSubs],\n };\n },\n });\n setCurrentPastPage(nextPage);\n } catch (err) {\n console.error('Failed to load more past subscriptions:', err);\n } finally {\n setIsLoadingMorePast(false);\n }\n }, [fetchMorePast, isLoadingMorePast, pastSubscriptionsPagination.hasMore, currentPastPage]);\n\n const combinedRefetch = useCallback(() => {\n setCurrentPastPage(1);\n if (shouldFetchActive) refetch();\n if (shouldFetchPast) refetchPast();\n }, [shouldFetchActive, shouldFetchPast, refetch, refetchPast]);\n\n return {\n activeSubscriptions,\n pastSubscriptions,\n isLoading: shouldFetchActive ? loading : false,\n isPastSubscriptionsLoading: shouldFetchPast ? pastLoading : false,\n error: error ?? null,\n refetch: combinedRefetch,\n // Pagination for past subscriptions\n loadMorePastSubscriptions,\n hasMorePastSubscriptions: pastSubscriptionsPagination.hasMore,\n isLoadingMorePast,\n pastSubscriptionsCount: pastSubscriptionsPagination.totalCount,\n };\n}\n\n/**\n * Hook to get quota usage details for drill-down using cursor-based pagination.\n *\n * The activity-svc API is cursor-based (limit / cursor → items / hasNextPage / nextCursor).\n * Callers should maintain a cursor stack:\n * - push nextCursor to go forward\n * - pop to go back\n * - pass null as cursor for the first page\n */\nexport interface QuotaUsageDetail {\n id: string;\n amount: number;\n description: string;\n tags?: Record<string, unknown>;\n productId?: string;\n userId?: string;\n timestamp: string;\n lastUpdated?: string;\n}\n\ninterface UseQuotaUsageDetailsResult {\n usages: QuotaUsageDetail[];\n hasNextPage: boolean;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\ntype UsageRecord = GetUsageRecordsQuery['getUsageRecords']['items'][number];\n\nexport function buildUsageRecordsVariables(\n quotaAssignmentId: string | undefined,\n pageSize: number,\n cursor: string | null,\n workspaceId?: string\n): GetUsageRecordsQueryVariables {\n return {\n filters: {\n quotaAssignmentId,\n workspaceId,\n },\n pagination: {\n limit: pageSize,\n ...(cursor ? { cursor } : {}),\n },\n };\n}\n\nexport function mapUsageRecordToQuotaUsageDetail(record: UsageRecord): QuotaUsageDetail {\n return {\n id: record.id,\n amount: record.amount,\n description: record.description ?? '',\n tags: (record.metadata as Record<string, unknown>) ?? {},\n productId: record.productId,\n userId: record.userId ?? undefined,\n timestamp: record.createdAt,\n lastUpdated: record.createdAt,\n };\n}\n\nexport function useQuotaUsageDetails(\n quotaAssignmentId: string | undefined,\n pageSize: number = 20,\n cursor: string | null = null,\n workspaceId?: string\n): UseQuotaUsageDetailsResult {\n // Query workspace gateway directly — usage records live in wspace-activity-svc.\n // Uses the dedicated workspace Apollo client so it doesn't conflict with the\n // shell's global Apollo client used by all other billing queries.\n const wsClient = useGatewayClient('workspace');\n const { data, loading, error, refetch } = useGetUsageRecordsQuery({\n client: wsClient,\n variables: buildUsageRecordsVariables(quotaAssignmentId, pageSize, cursor, workspaceId),\n skip: !quotaAssignmentId || !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n\n const items: GetUsageRecordsQuery['getUsageRecords']['items'] =\n data?.getUsageRecords?.items ?? [];\n\n return {\n usages: items.map(mapUsageRecordToQuotaUsageDetail),\n hasNextPage: data?.getUsageRecords?.hasNextPage ?? false,\n nextCursor: data?.getUsageRecords?.nextCursor ?? null,\n isLoading: loading,\n error: error ?? null,\n refetch,\n };\n}\n\n/**\n * Hook to get quota analytics\n */\ninterface UseQuotaAnalyticsResult {\n analytics: {\n dailyUsage: Array<{ date: string; usage: number; count: number }>;\n usageByType: Array<{ resourceType: string; usage: number; count: number; percentage: number }>;\n trends?: {\n growthRate?: number;\n predictedUsage?: number;\n averageDailyUsage?: number;\n };\n peakUsage?: {\n maxDailyUsage: number;\n peakDate: string;\n peakHour?: number;\n };\n } | null;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useQuotaAnalytics(\n quotaAssignmentId: string | undefined,\n period: AnalyticsPeriod = AnalyticsPeriod.LAST_30_DAYS\n): UseQuotaAnalyticsResult {\n const { data, loading, error } = useGetQuotaAnalyticsForUsageQuery({\n variables: { quotaAssignmentId: quotaAssignmentId!, period },\n skip: !quotaAssignmentId,\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n analytics: data?.getQuotaAnalytics ?? null,\n isLoading: loading,\n error: error ?? null,\n };\n}\n\n/**\n * Combined hook for the enhanced usage page\n */\ninterface UseEnhancedUsagePageParams {\n billingAccountId: string | undefined;\n workspaceId?: string;\n period?: AnalyticsPeriod;\n searchQuery?: string;\n viewMode?: ViewMode;\n}\n\ninterface UseEnhancedUsagePageResult {\n // Dashboard overview\n dashboard: UsageDashboard | null;\n\n // Pooled quotas (subscription quotas come from subscriptions directly)\n pooledQuotas: AggregatedQuota[];\n\n // Subscriptions with their quotas\n activeSubscriptions: SubscriptionWithAggregatedQuotas[];\n pastSubscriptions: SubscriptionWithAggregatedQuotas[];\n\n // Loading states\n isLoading: boolean;\n isPastSubscriptionsLoading: boolean;\n error: Error | null;\n\n // Actions\n refetch: () => void;\n\n // Pagination for past subscriptions\n loadMorePastSubscriptions: () => void;\n hasMorePastSubscriptions: boolean;\n isLoadingMorePast: boolean;\n pastSubscriptionsCount: number;\n}\n\nexport function useEnhancedUsagePage({\n billingAccountId,\n workspaceId,\n period = AnalyticsPeriod.LAST_30_DAYS,\n searchQuery,\n viewMode = 'active',\n}: UseEnhancedUsagePageParams): UseEnhancedUsagePageResult {\n // Dashboard for overview stats\n const dashboardResult = useUsageDashboard({\n billingAccountId,\n period,\n });\n\n // Quotas summary with aggregation\n const quotasSummaryResult = useBillingAccountQuotasSummary(billingAccountId);\n\n // Live collector values from activity-svc (source of truth for count/concurrent quotas)\n const { statusMap } = useWorkspaceQuotaStatuses(workspaceId);\n\n // Subscriptions with quotas - passes searchQuery and viewMode to backend\n const subscriptionsResult = useSubscriptionsWithQuotas(billingAccountId, {\n searchQuery,\n viewMode,\n });\n\n // Merge live values into both pooled quotas and subscription quotas\n const pooledQuotas = useMemo(\n () => mergeLiveValues(quotasSummaryResult.pooledQuotas, statusMap),\n [quotasSummaryResult.pooledQuotas, statusMap]\n );\n\n const activeSubscriptions = useMemo(\n () =>\n subscriptionsResult.activeSubscriptions.map((sub) => ({\n ...sub,\n aggregatedQuotas: mergeLiveValues(sub.aggregatedQuotas, statusMap),\n })),\n [subscriptionsResult.activeSubscriptions, statusMap]\n );\n\n const pastSubscriptions = useMemo(\n () =>\n subscriptionsResult.pastSubscriptions.map((sub) => ({\n ...sub,\n aggregatedQuotas: mergeLiveValues(sub.aggregatedQuotas, statusMap),\n })),\n [subscriptionsResult.pastSubscriptions, statusMap]\n );\n\n const refetch = useCallback(() => {\n dashboardResult.refetch();\n quotasSummaryResult.refetch();\n subscriptionsResult.refetch();\n }, [dashboardResult, quotasSummaryResult, subscriptionsResult]);\n\n return {\n dashboard: dashboardResult.dashboard,\n pooledQuotas,\n activeSubscriptions,\n pastSubscriptions,\n isLoading:\n dashboardResult.isLoading || quotasSummaryResult.isLoading || subscriptionsResult.isLoading,\n isPastSubscriptionsLoading: subscriptionsResult.isPastSubscriptionsLoading,\n error: dashboardResult.error || quotasSummaryResult.error || subscriptionsResult.error,\n refetch,\n // Pagination for past subscriptions\n loadMorePastSubscriptions: subscriptionsResult.loadMorePastSubscriptions,\n hasMorePastSubscriptions: subscriptionsResult.hasMorePastSubscriptions,\n isLoadingMorePast: subscriptionsResult.isLoadingMorePast,\n pastSubscriptionsCount: subscriptionsResult.pastSubscriptionsCount,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useUsage.js","names":[],"sources":["../../../../../src/billing/modules/usage/hooks/useUsage.ts"],"sourcesContent":["/**\n * Usage Module - Hooks\n * Custom hooks for usage data fetching and mutations\n */\n\nimport { useMemo, useCallback, useState, useEffect } from 'react';\nimport {\n useGetUsageDashboardQuery,\n useGetBillingAccountQuotasSummaryForUsageQuery,\n useGetSubscriptionsWithQuotasQuery,\n useGetPastSubscriptionsWithQuotasQuery,\n useGetBillingAccountsForUsageQuery,\n useGetQuotaAnalyticsForUsageQuery,\n type GetPastSubscriptionsWithQuotasQuery,\n} from '../../../../generated/global-operations';\nimport {\n useGetUsageRecordsQuery,\n type GetUsageRecordsQuery,\n type GetUsageRecordsQueryVariables,\n} from '../../../../generated/wspace-operations';\nimport { useGatewayClient, useQuery, useMutation, gql } from '@burdenoff/fe-libs/shared/graphql';\nimport { AnalyticsPeriod, QuotaStatus } from '../../../shared/types';\nimport type { UsageDashboard } from '../../../shared/types';\n\n// ============================================================================\n// Workspace Quota Assignment — managed by activity-svc\n// ============================================================================\n\nconst WORKSPACE_QUOTA_OVERVIEW_QUERY = gql`\n query WorkspaceQuotaOverview($workspaceId: ID!) {\n workspaceQuotaOverview(workspaceId: $workspaceId) {\n assignment {\n id\n workspaceId\n quotaName\n productId\n quotaMode\n billingAccountId\n subscriptionId\n limit\n used\n noLimit\n resetPeriod\n lastResetAt\n isActive\n createdAt\n }\n liveStatus {\n workspaceId\n quotaName\n productId\n currentValue\n limit\n noLimit\n scrapedAt\n }\n effectiveUsed\n remaining\n usagePercentage\n }\n }\n`;\n\nconst UNASSIGN_WORKSPACE_QUOTA_MUTATION = gql`\n mutation UnassignWorkspaceQuota($id: ID!, $workspaceId: ID!) {\n unassignWorkspaceQuota(id: $id, workspaceId: $workspaceId)\n }\n`;\n\nexport interface WorkspaceQuotaAssignmentItem {\n id: string;\n workspaceId: string;\n quotaName: string;\n productId: string;\n quotaMode: 'dedicated' | 'pooled';\n billingAccountId: string | null;\n subscriptionId: string | null;\n limit: number | null;\n used: number;\n noLimit: boolean;\n resetPeriod: string;\n lastResetAt: string | null;\n isActive: boolean;\n createdAt: string;\n}\n\nexport interface WorkspaceQuotaOverviewItem {\n assignment: WorkspaceQuotaAssignmentItem;\n liveStatus: WorkspaceQuotaStatusItem | null;\n effectiveUsed: number;\n remaining: number | null;\n usagePercentage: number;\n}\n\nexport interface AssignWorkspaceQuotaInput {\n workspaceId: string;\n quotaName: string;\n productId: string;\n quotaMode?: 'dedicated' | 'pooled';\n billingAccountId?: string;\n subscriptionId?: string;\n limit?: number;\n noLimit?: boolean;\n resetPeriod?: string;\n metadata?: Record<string, unknown>;\n}\n\ninterface UseWorkspaceQuotaOverviewResult {\n overview: WorkspaceQuotaOverviewItem[];\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useWorkspaceQuotaOverview(workspaceId?: string): UseWorkspaceQuotaOverviewResult {\n const wsClient = useGatewayClient('workspace');\n const { data, loading, error, refetch } = useQuery<{\n workspaceQuotaOverview: WorkspaceQuotaOverviewItem[];\n }>(WORKSPACE_QUOTA_OVERVIEW_QUERY, {\n client: wsClient,\n variables: { workspaceId },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n overview: data?.workspaceQuotaOverview ?? [],\n isLoading: loading,\n error: error ?? null,\n refetch,\n };\n}\n\n// ============================================================================\n// activateSubscriptionForWorkspace — single backend call that assigns all\n// WORKSPACE-scoped quotas from the purchased plan. Replaces the N individual\n// assignWorkspaceQuota calls so the UI never sends quota names/limits to the server.\n// ============================================================================\n\nconst ACTIVATE_SUBSCRIPTION_FOR_WORKSPACE_MUTATION = gql`\n mutation ActivateSubscriptionForWorkspace(\n $subscriptionId: ID!\n $billingAccountId: ID!\n $workspaceId: ID!\n ) {\n activateSubscriptionForWorkspace(\n subscriptionId: $subscriptionId\n billingAccountId: $billingAccountId\n workspaceId: $workspaceId\n ) {\n workspaceId\n assignedQuotas\n skippedQuotas\n }\n }\n`;\n\nexport interface ActivateSubscriptionForWorkspaceResult {\n workspaceId: string;\n assignedQuotas: number;\n skippedQuotas: number;\n}\n\ninterface UseActivateSubscriptionForWorkspaceResult {\n activate: (\n subscriptionId: string,\n billingAccountId: string,\n workspaceId: string\n ) => Promise<ActivateSubscriptionForWorkspaceResult | null>;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useActivateSubscriptionForWorkspace(): UseActivateSubscriptionForWorkspaceResult {\n const globalClient = useGatewayClient('global');\n const [mutate, { loading, error }] = useMutation<\n { activateSubscriptionForWorkspace: ActivateSubscriptionForWorkspaceResult },\n { subscriptionId: string; billingAccountId: string; workspaceId: string }\n >(ACTIVATE_SUBSCRIPTION_FOR_WORKSPACE_MUTATION, { client: globalClient });\n\n const activate = async (\n subscriptionId: string,\n billingAccountId: string,\n workspaceId: string\n ) => {\n const result = await mutate({ variables: { subscriptionId, billingAccountId, workspaceId } });\n return result.data?.activateSubscriptionForWorkspace ?? null;\n };\n\n return { activate, isLoading: loading, error: error ?? null };\n}\n\ninterface UseUnassignWorkspaceQuotaResult {\n unassign: (id: string, workspaceId: string) => Promise<boolean>;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useUnassignWorkspaceQuota(): UseUnassignWorkspaceQuotaResult {\n const wsClient = useGatewayClient('workspace');\n const [mutate, { loading, error }] = useMutation<\n { unassignWorkspaceQuota: boolean },\n { id: string; workspaceId: string }\n >(UNASSIGN_WORKSPACE_QUOTA_MUTATION, { client: wsClient });\n\n const unassign = async (id: string, workspaceId: string) => {\n const result = await mutate({ variables: { id, workspaceId } });\n return result.data?.unassignWorkspaceQuota ?? false;\n };\n\n return { unassign, isLoading: loading, error: error ?? null };\n}\n\n// ============================================================================\n// Workspace Quota Status — live values from activity-svc\n// ============================================================================\n\nconst WORKSPACE_QUOTA_STATUSES_QUERY = gql`\n query WorkspaceQuotaStatuses($workspaceId: ID!) {\n workspaceQuotaStatuses(workspaceId: $workspaceId) {\n workspaceId\n quotaName\n productId\n currentValue\n limit\n noLimit\n scrapedAt\n }\n }\n`;\n\nconst WORKSPACE_QUOTA_HISTORY_QUERY = gql`\n query WorkspaceQuotaHistory($workspaceId: ID!, $quotaName: String!, $limit: Int) {\n workspaceQuotaHistory(workspaceId: $workspaceId, quotaName: $quotaName, limit: $limit) {\n id\n workspaceId\n quotaName\n productId\n value\n limit\n noLimit\n snapshotAt\n }\n }\n`;\n\nexport interface WorkspaceQuotaStatusItem {\n workspaceId: string;\n quotaName: string;\n productId: string;\n currentValue: number;\n limit: number | null;\n noLimit: boolean;\n scrapedAt: string;\n}\n\nexport interface WorkspaceQuotaSnapshotItem {\n id: string;\n workspaceId: string;\n quotaName: string;\n productId: string;\n value: number;\n limit: number | null;\n noLimit: boolean;\n snapshotAt: string;\n}\n\ninterface UseWorkspaceQuotaStatusesResult {\n statuses: WorkspaceQuotaStatusItem[];\n statusMap: Map<string, WorkspaceQuotaStatusItem>;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useWorkspaceQuotaStatuses(workspaceId?: string): UseWorkspaceQuotaStatusesResult {\n const wsClient = useGatewayClient('workspace');\n const { data, loading, error } = useQuery<{ workspaceQuotaStatuses: WorkspaceQuotaStatusItem[] }>(\n WORKSPACE_QUOTA_STATUSES_QUERY,\n {\n client: wsClient,\n variables: { workspaceId },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n }\n );\n\n const { statuses, statusMap } = useMemo(() => {\n const list = data?.workspaceQuotaStatuses ?? [];\n return { statuses: list, statusMap: new Map(list.map((s) => [s.quotaName, s])) };\n }, [data]);\n\n return { statuses, statusMap, isLoading: loading, error: error ?? null };\n}\n\ninterface UseWorkspaceQuotaHistoryResult {\n snapshots: WorkspaceQuotaSnapshotItem[];\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useWorkspaceQuotaHistory(\n workspaceId?: string,\n quotaName?: string,\n limit: number = 48\n): UseWorkspaceQuotaHistoryResult {\n const wsClient = useGatewayClient('workspace');\n const { data, loading, error } = useQuery<{\n workspaceQuotaHistory: WorkspaceQuotaSnapshotItem[];\n }>(WORKSPACE_QUOTA_HISTORY_QUERY, {\n client: wsClient,\n variables: { workspaceId, quotaName, limit },\n skip: !workspaceId || !quotaName,\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n snapshots: data?.workspaceQuotaHistory ?? [],\n isLoading: loading,\n error: error ?? null,\n };\n}\n\n// ============================================================================\n// Dashboard Hook\n// ============================================================================\n\ninterface UseUsageDashboardParams {\n billingAccountId?: string;\n period?: AnalyticsPeriod;\n}\n\ninterface UseUsageDashboardResult {\n dashboard: UsageDashboard | null;\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useUsageDashboard({\n billingAccountId,\n period = AnalyticsPeriod.LAST_30_DAYS,\n}: UseUsageDashboardParams): UseUsageDashboardResult {\n const { data, loading, error, refetch } = useGetUsageDashboardQuery({\n variables: { billingAccountId: billingAccountId!, period },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n dashboard: data?.getUsageDashboard ?? null,\n isLoading: loading,\n error: error ?? null,\n refetch,\n };\n}\n\n// ============================================================================\n// Enhanced Usage Page Hooks\n// ============================================================================\n\n/**\n * Quota assignment from the API\n */\ninterface QuotaAssignmentSummary {\n id: string;\n name: string;\n unit: string;\n quotaType: 'POOLED' | 'PRODUCT_SPECIFIC';\n reusable: boolean;\n limit: number;\n limitLeft: number;\n used: number;\n usagePercentage: number;\n noLimit: boolean;\n subscriptionId?: string;\n context?: Record<string, unknown> | null;\n recentUsages: Array<{\n id: string;\n amount: number;\n description: string;\n timestamp: string;\n tags?: Record<string, unknown>;\n }>;\n createdAt: string;\n endtime: string;\n}\n\n/**\n * Aggregated quota for display (multiple quota assignments with same name combined)\n */\nexport interface AggregatedQuota {\n name: string;\n displayName?: string;\n description?: string;\n unit: string;\n totalLimit: number;\n totalUsed: number;\n usagePercentage: number;\n status: QuotaStatus;\n quotaCount: number;\n noLimit: boolean;\n reusable: boolean;\n /** True when usage comes from the activity-svc scraper, not billing recordUsage events. */\n isCollectorBased: boolean;\n quotaAssignments: QuotaAssignmentSummary[];\n}\n\n/**\n * Subscription with aggregated quotas\n */\nexport interface SubscriptionWithAggregatedQuotas {\n id: string;\n status: string;\n planName?: string;\n productId?: string;\n createdAt: string;\n aggregatedQuotas: AggregatedQuota[];\n}\n\n/**\n * Helper function to calculate quota status based on usage percentage\n */\nfunction calculateQuotaStatus(\n usagePercentage: number,\n limitLeft: number,\n noLimit = false\n): QuotaStatus {\n if (noLimit) return QuotaStatus.HEALTHY;\n if (limitLeft <= 0) return QuotaStatus.EXHAUSTED;\n if (usagePercentage >= 90) return QuotaStatus.CRITICAL;\n if (usagePercentage >= 75) return QuotaStatus.WARNING;\n return QuotaStatus.HEALTHY;\n}\n\n// Maps billing quota names to their corresponding scraper metric names in workspace_quota_statuses.\n// Scraper keys use dot-notation product prefix; billing keys use kebab-case.\nconst BILLING_TO_SCRAPER_METRIC: Record<string, string> = {\n 'agents-registered': 'vibecontrols.agents.count',\n 'vibes-created': 'vibecontrols.vibes.count',\n 'catalogs-created': 'vibecontrols.catalogs.count',\n 'tunnels-created': 'vibecontrols.tunnels.count',\n 'sessions-started': 'vibecontrols.sessions.starts',\n};\n\n/**\n * Merges live collector values from activity-svc into a list of aggregated quotas.\n * Overrides `totalUsed` (and derived fields) when a live value exists, and distributes\n * the live total across individual assignments (earliest-expiry first) so the\n * Quota Breakdown shows real per-assignment consumption instead of always-0.\n */\nfunction mergeLiveValues(\n aggregated: AggregatedQuota[],\n statusMap: Map<string, WorkspaceQuotaStatusItem>\n): AggregatedQuota[] {\n if (statusMap.size === 0) return aggregated;\n return aggregated.map((q) => {\n const scraperKey = BILLING_TO_SCRAPER_METRIC[q.name] ?? q.name;\n const live = statusMap.get(scraperKey);\n if (!live) return q;\n\n const totalUsed = live.currentValue;\n const { totalLimit, noLimit } = q;\n const usagePercentage = noLimit || totalLimit === 0 ? 0 : (totalUsed / totalLimit) * 100;\n const limitLeft = noLimit ? 0 : Math.max(0, totalLimit - totalUsed);\n\n // Distribute live total into assignments: fill earliest-expiry first, overflow to next.\n let remaining = totalUsed;\n const updatedAssignments = [...q.quotaAssignments]\n .sort((a, b) => {\n const dateA = a.endtime ? new Date(a.endtime).getTime() : Infinity;\n const dateB = b.endtime ? new Date(b.endtime).getTime() : Infinity;\n return dateA - dateB;\n })\n .map((a) => {\n const assignmentUsed = Math.min(remaining, a.limit || 0);\n remaining = Math.max(0, remaining - assignmentUsed);\n return { ...a, used: assignmentUsed };\n });\n\n return {\n ...q,\n totalUsed,\n usagePercentage,\n isCollectorBased: true,\n status: calculateQuotaStatus(usagePercentage, limitLeft, noLimit),\n quotaAssignments: updatedAssignments,\n };\n });\n}\n\n/**\n * Helper function to aggregate quotas by name\n */\nfunction aggregateQuotasByName(quotas: QuotaAssignmentSummary[]): AggregatedQuota[] {\n const grouped = new Map<string, QuotaAssignmentSummary[]>();\n\n for (const quota of quotas) {\n const existing = grouped.get(quota.name) || [];\n existing.push(quota);\n grouped.set(quota.name, existing);\n }\n\n const aggregated: AggregatedQuota[] = [];\n\n for (const [name, assignments] of grouped) {\n // Sum up limits and usage first\n const totalLimit = assignments.reduce((sum, q) => sum + (q.limit || 0), 0);\n const totalUsed = assignments.reduce((sum, q) => sum + (q.used || 0), 0);\n const totalLimitLeft = assignments.reduce((sum, q) => sum + (q.limitLeft || 0), 0);\n\n // Check if there's actually no limit (ignore noLimit flag if there's a numeric limit)\n // Only treat as unlimited if totalLimit is 0 or all assignments explicitly have noLimit AND no numeric limit\n const hasNoLimit =\n totalLimit === 0 && assignments.every((q) => q.noLimit || (q.limit || 0) === 0);\n\n // Calculate percentage\n const usagePercentage = hasNoLimit || totalLimit === 0 ? 0 : (totalUsed / totalLimit) * 100;\n\n // Get status\n const status = calculateQuotaStatus(usagePercentage, totalLimitLeft, hasNoLimit);\n\n // Get reusable, unit, and display info from first assignment\n const reusable = assignments[0]?.reusable || false;\n const unit = assignments[0]?.unit || '';\n const ctx = assignments[0]?.context;\n const displayName = typeof ctx?.displayName === 'string' ? ctx.displayName : undefined;\n const description = typeof ctx?.description === 'string' ? ctx.description : undefined;\n\n aggregated.push({\n name,\n displayName,\n description,\n unit,\n totalLimit: hasNoLimit ? 0 : totalLimit,\n totalUsed,\n usagePercentage,\n status,\n quotaCount: assignments.length,\n noLimit: hasNoLimit,\n isCollectorBased: false,\n reusable,\n quotaAssignments: assignments,\n });\n }\n\n // Sort by status priority (critical first) then by usage percentage\n const statusPriority: Record<QuotaStatus, number> = {\n EXHAUSTED: 0,\n CRITICAL: 1,\n WARNING: 2,\n HEALTHY: 3,\n INACTIVE: 4,\n };\n\n return aggregated.sort((a, b) => {\n const priorityDiff = statusPriority[a.status] - statusPriority[b.status];\n if (priorityDiff !== 0) return priorityDiff;\n return b.usagePercentage - a.usagePercentage;\n });\n}\n\n/**\n * Hook to get billing accounts for the selector\n */\ninterface UseBillingAccountsForUsageResult {\n billingAccounts: Array<{\n id: string;\n name: string;\n email: string;\n currency: string;\n creditAmount: number;\n isActive: boolean;\n isDefault: boolean;\n }>;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useBillingAccountsForUsage(): UseBillingAccountsForUsageResult {\n const { data, loading, error } = useGetBillingAccountsForUsageQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n billingAccounts: data?.getBillingAccountsByOrg ?? [],\n isLoading: loading,\n error: error ?? null,\n };\n}\n\n/**\n * Hook to get billing account quotas summary with aggregation\n * Only returns pooled quotas - subscription quotas come from useSubscriptionsWithQuotas\n */\ninterface UseBillingAccountQuotasSummaryResult {\n pooledQuotas: AggregatedQuota[];\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useBillingAccountQuotasSummary(\n billingAccountId: string | undefined,\n productId?: string\n): UseBillingAccountQuotasSummaryResult {\n const { data, loading, error, refetch } = useGetBillingAccountQuotasSummaryForUsageQuery({\n variables: { billingAccountId: billingAccountId!, productId },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const pooledQuotas = useMemo(() => {\n if (!data?.getBillingAccountQuotasSummary) {\n return [];\n }\n\n const summary = data.getBillingAccountQuotasSummary;\n return aggregateQuotasByName(summary.pooledQuotas || []);\n }, [data]);\n\n return {\n pooledQuotas,\n isLoading: loading,\n error: error ?? null,\n refetch,\n };\n}\n\n/**\n * Subscription from the API\n */\ninterface SubscriptionQuota {\n id: string;\n name: string;\n limits: { type: string; value: number; unit?: string; originalValue?: number };\n context?: Record<string, unknown> | null;\n currentUsageSum: number;\n usageCount: number;\n createdAt: string;\n endtime: string;\n}\n\ninterface SubscriptionFromAPI {\n id: string;\n status: string;\n createdAt: string;\n quotas: SubscriptionQuota[];\n}\n\n/**\n * Hook to get subscriptions with their quotas grouped by status\n * Uses lazy loading for past subscriptions and supports backend search\n */\ntype ViewMode = 'active' | 'past' | 'all';\n\nconst PAST_SUBSCRIPTIONS_PAGE_SIZE = 10;\n\ninterface UseSubscriptionsWithQuotasOptions {\n searchQuery?: string;\n viewMode?: ViewMode;\n}\n\ninterface UseSubscriptionsWithQuotasResult {\n activeSubscriptions: SubscriptionWithAggregatedQuotas[];\n pastSubscriptions: SubscriptionWithAggregatedQuotas[];\n isLoading: boolean;\n isPastSubscriptionsLoading: boolean;\n error: Error | null;\n refetch: () => void;\n // Pagination for past subscriptions\n loadMorePastSubscriptions: () => void;\n hasMorePastSubscriptions: boolean;\n isLoadingMorePast: boolean;\n pastSubscriptionsCount: number;\n}\n\nexport function useSubscriptionsWithQuotas(\n billingAccountId: string | undefined,\n options: UseSubscriptionsWithQuotasOptions = {}\n): UseSubscriptionsWithQuotasResult {\n const { searchQuery, viewMode = 'active' } = options;\n\n // Determine which subscriptions to fetch based on viewMode\n const shouldFetchActive = viewMode === 'active' || viewMode === 'all';\n const shouldFetchPast = viewMode === 'past' || viewMode === 'all';\n\n // Query for active subscriptions\n const { data, loading, error, refetch } = useGetSubscriptionsWithQuotasQuery({\n variables: {\n billingAccountId: billingAccountId!,\n searchQuery: searchQuery || null,\n },\n skip: !billingAccountId || !shouldFetchActive,\n fetchPolicy: 'cache-and-network',\n });\n\n // Query for past subscriptions with pagination - only when viewMode is 'past' or 'all'\n const {\n data: pastData,\n loading: pastLoading,\n refetch: refetchPast,\n fetchMore: fetchMorePast,\n } = useGetPastSubscriptionsWithQuotasQuery({\n variables: {\n billingAccountId: billingAccountId!,\n searchQuery: searchQuery || null,\n page: 1,\n pageSize: PAST_SUBSCRIPTIONS_PAGE_SIZE,\n },\n skip: !billingAccountId || !shouldFetchPast,\n fetchPolicy: 'cache-and-network',\n });\n\n // Track if we're loading more past subscriptions\n const [isLoadingMorePast, setIsLoadingMorePast] = useState(false);\n const [currentPastPage, setCurrentPastPage] = useState(1);\n\n // Helper function to process subscription data\n const processSubscription = useCallback(\n (sub: SubscriptionFromAPI): SubscriptionWithAggregatedQuotas => {\n const quotaSummaries: QuotaAssignmentSummary[] = (sub.quotas || []).map((q) => {\n const limit = q.limits?.value || 0;\n const used = q.currentUsageSum || 0;\n const limitLeft = Math.max(0, limit - used);\n const usagePercentage = limit > 0 ? (used / limit) * 100 : 0;\n const unit = q.limits?.unit || '';\n\n return {\n id: q.id,\n name: q.name,\n unit,\n quotaType: 'PRODUCT_SPECIFIC' as const,\n reusable: false,\n limit,\n limitLeft,\n used,\n usagePercentage,\n noLimit: q.limits?.type !== 'number' && q.limits?.type !== 'numeric',\n subscriptionId: sub.id,\n context: q.context,\n recentUsages: [],\n createdAt: q.createdAt,\n endtime: q.endtime,\n };\n });\n\n return {\n id: sub.id,\n status: sub.status,\n createdAt: sub.createdAt,\n aggregatedQuotas: aggregateQuotasByName(quotaSummaries),\n };\n },\n []\n );\n\n // Process active subscriptions\n const activeSubscriptions = useMemo(() => {\n if (!data?.getSubscriptionUsageOverview) {\n return [];\n }\n\n const subscriptions: SubscriptionFromAPI[] = data.getSubscriptionUsageOverview;\n return subscriptions.map(processSubscription);\n }, [data, processSubscription]);\n\n // Process past subscriptions\n const pastSubscriptions = useMemo(() => {\n if (!pastData?.getSubscriptionUsageOverview) {\n return [];\n }\n\n const subscriptions: SubscriptionFromAPI[] =\n pastData.getSubscriptionUsageOverview.subscriptions || pastData.getSubscriptionUsageOverview;\n return subscriptions.map(processSubscription);\n }, [pastData, processSubscription]);\n\n // Get pagination info for past subscriptions\n const pastSubscriptionsPagination = useMemo(() => {\n const overview = pastData?.getSubscriptionUsageOverview;\n if (!overview) {\n return { totalCount: 0, hasMore: false };\n }\n // If it's the new paginated format\n if (overview.totalCount !== undefined) {\n return {\n totalCount: overview.totalCount,\n hasMore: pastSubscriptions.length < overview.totalCount,\n };\n }\n // Legacy format - assume there might be more if we got a full page\n return {\n totalCount: pastSubscriptions.length,\n hasMore: pastSubscriptions.length >= PAST_SUBSCRIPTIONS_PAGE_SIZE * currentPastPage,\n };\n }, [pastData, pastSubscriptions.length, currentPastPage]);\n\n // Reset page when search query or viewMode changes\n useEffect(() => {\n setCurrentPastPage(1);\n }, [searchQuery, viewMode]);\n\n // Load more past subscriptions\n const loadMorePastSubscriptions = useCallback(async () => {\n if (!fetchMorePast || isLoadingMorePast || !pastSubscriptionsPagination.hasMore) return;\n\n setIsLoadingMorePast(true);\n const nextPage = currentPastPage + 1;\n\n try {\n await fetchMorePast({\n variables: {\n page: nextPage,\n pageSize: PAST_SUBSCRIPTIONS_PAGE_SIZE,\n },\n updateQuery: (\n prev: GetPastSubscriptionsWithQuotasQuery,\n { fetchMoreResult }: { fetchMoreResult: GetPastSubscriptionsWithQuotasQuery }\n ) => {\n if (!fetchMoreResult) return prev;\n\n const prevSubs = prev?.getSubscriptionUsageOverview ?? [];\n const newSubs = fetchMoreResult?.getSubscriptionUsageOverview ?? [];\n\n return {\n getSubscriptionUsageOverview: [...prevSubs, ...newSubs],\n };\n },\n });\n setCurrentPastPage(nextPage);\n } catch (err) {\n console.error('Failed to load more past subscriptions:', err);\n } finally {\n setIsLoadingMorePast(false);\n }\n }, [fetchMorePast, isLoadingMorePast, pastSubscriptionsPagination.hasMore, currentPastPage]);\n\n const combinedRefetch = useCallback(() => {\n setCurrentPastPage(1);\n if (shouldFetchActive) refetch();\n if (shouldFetchPast) refetchPast();\n }, [shouldFetchActive, shouldFetchPast, refetch, refetchPast]);\n\n return {\n activeSubscriptions,\n pastSubscriptions,\n isLoading: shouldFetchActive ? loading : false,\n isPastSubscriptionsLoading: shouldFetchPast ? pastLoading : false,\n error: error ?? null,\n refetch: combinedRefetch,\n // Pagination for past subscriptions\n loadMorePastSubscriptions,\n hasMorePastSubscriptions: pastSubscriptionsPagination.hasMore,\n isLoadingMorePast,\n pastSubscriptionsCount: pastSubscriptionsPagination.totalCount,\n };\n}\n\n/**\n * Hook to get quota usage details for drill-down using cursor-based pagination.\n *\n * The activity-svc API is cursor-based (limit / cursor → items / hasNextPage / nextCursor).\n * Callers should maintain a cursor stack:\n * - push nextCursor to go forward\n * - pop to go back\n * - pass null as cursor for the first page\n */\nexport interface QuotaUsageDetail {\n id: string;\n amount: number;\n description: string;\n tags?: Record<string, unknown>;\n productId?: string;\n userId?: string;\n timestamp: string;\n lastUpdated?: string;\n}\n\ninterface UseQuotaUsageDetailsResult {\n usages: QuotaUsageDetail[];\n hasNextPage: boolean;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\ntype UsageRecord = GetUsageRecordsQuery['getUsageRecords']['items'][number];\n\nexport function buildUsageRecordsVariables(\n quotaAssignmentId: string | undefined,\n pageSize: number,\n cursor: string | null,\n workspaceId?: string\n): GetUsageRecordsQueryVariables {\n return {\n filters: {\n quotaAssignmentId,\n workspaceId,\n },\n pagination: {\n limit: pageSize,\n ...(cursor ? { cursor } : {}),\n },\n };\n}\n\nexport function mapUsageRecordToQuotaUsageDetail(record: UsageRecord): QuotaUsageDetail {\n return {\n id: record.id,\n amount: record.amount,\n description: record.description ?? '',\n tags: (record.metadata as Record<string, unknown>) ?? {},\n productId: record.productId,\n userId: record.userId ?? undefined,\n timestamp: record.createdAt,\n lastUpdated: record.createdAt,\n };\n}\n\nexport function useQuotaUsageDetails(\n quotaAssignmentId: string | undefined,\n pageSize: number = 20,\n cursor: string | null = null,\n workspaceId?: string\n): UseQuotaUsageDetailsResult {\n // Query workspace gateway directly — usage records live in wspace-activity-svc.\n // Uses the dedicated workspace Apollo client so it doesn't conflict with the\n // shell's global Apollo client used by all other billing queries.\n const wsClient = useGatewayClient('workspace');\n const { data, loading, error, refetch } = useGetUsageRecordsQuery({\n client: wsClient,\n variables: buildUsageRecordsVariables(quotaAssignmentId, pageSize, cursor, workspaceId),\n skip: !quotaAssignmentId || !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n\n const items: GetUsageRecordsQuery['getUsageRecords']['items'] =\n data?.getUsageRecords?.items ?? [];\n\n return {\n usages: items.map(mapUsageRecordToQuotaUsageDetail),\n hasNextPage: data?.getUsageRecords?.hasNextPage ?? false,\n nextCursor: data?.getUsageRecords?.nextCursor ?? null,\n isLoading: loading,\n error: error ?? null,\n refetch,\n };\n}\n\n/**\n * Hook to get quota analytics\n */\ninterface UseQuotaAnalyticsResult {\n analytics: {\n dailyUsage: Array<{ date: string; usage: number; count: number }>;\n usageByType: Array<{ resourceType: string; usage: number; count: number; percentage: number }>;\n trends?: {\n growthRate?: number;\n predictedUsage?: number;\n averageDailyUsage?: number;\n };\n peakUsage?: {\n maxDailyUsage: number;\n peakDate: string;\n peakHour?: number;\n };\n } | null;\n isLoading: boolean;\n error: Error | null;\n}\n\nexport function useQuotaAnalytics(\n quotaAssignmentId: string | undefined,\n period: AnalyticsPeriod = AnalyticsPeriod.LAST_30_DAYS\n): UseQuotaAnalyticsResult {\n const { data, loading, error } = useGetQuotaAnalyticsForUsageQuery({\n variables: { quotaAssignmentId: quotaAssignmentId!, period },\n skip: !quotaAssignmentId,\n fetchPolicy: 'cache-and-network',\n });\n\n return {\n analytics: data?.getQuotaAnalytics ?? null,\n isLoading: loading,\n error: error ?? null,\n };\n}\n\n/**\n * Combined hook for the enhanced usage page\n */\ninterface UseEnhancedUsagePageParams {\n billingAccountId: string | undefined;\n workspaceId?: string;\n period?: AnalyticsPeriod;\n searchQuery?: string;\n viewMode?: ViewMode;\n}\n\ninterface UseEnhancedUsagePageResult {\n // Dashboard overview\n dashboard: UsageDashboard | null;\n\n // Pooled quotas (subscription quotas come from subscriptions directly)\n pooledQuotas: AggregatedQuota[];\n\n // Subscriptions with their quotas\n activeSubscriptions: SubscriptionWithAggregatedQuotas[];\n pastSubscriptions: SubscriptionWithAggregatedQuotas[];\n\n // Loading states\n isLoading: boolean;\n isPastSubscriptionsLoading: boolean;\n error: Error | null;\n\n // Actions\n refetch: () => void;\n\n // Pagination for past subscriptions\n loadMorePastSubscriptions: () => void;\n hasMorePastSubscriptions: boolean;\n isLoadingMorePast: boolean;\n pastSubscriptionsCount: number;\n}\n\nexport function useEnhancedUsagePage({\n billingAccountId,\n workspaceId,\n period = AnalyticsPeriod.LAST_30_DAYS,\n searchQuery,\n viewMode = 'active',\n}: UseEnhancedUsagePageParams): UseEnhancedUsagePageResult {\n // Dashboard for overview stats\n const dashboardResult = useUsageDashboard({\n billingAccountId,\n period,\n });\n\n // Quotas summary with aggregation\n const quotasSummaryResult = useBillingAccountQuotasSummary(billingAccountId);\n\n // Live collector values from activity-svc (source of truth for count/concurrent quotas)\n const { statusMap } = useWorkspaceQuotaStatuses(workspaceId);\n\n // Subscriptions with quotas - passes searchQuery and viewMode to backend\n const subscriptionsResult = useSubscriptionsWithQuotas(billingAccountId, {\n searchQuery,\n viewMode,\n });\n\n // Merge live values into both pooled quotas and subscription quotas\n const pooledQuotas = useMemo(\n () => mergeLiveValues(quotasSummaryResult.pooledQuotas, statusMap),\n [quotasSummaryResult.pooledQuotas, statusMap]\n );\n\n const activeSubscriptions = useMemo(\n () =>\n subscriptionsResult.activeSubscriptions.map((sub) => ({\n ...sub,\n aggregatedQuotas: mergeLiveValues(sub.aggregatedQuotas, statusMap),\n })),\n [subscriptionsResult.activeSubscriptions, statusMap]\n );\n\n const pastSubscriptions = useMemo(\n () =>\n subscriptionsResult.pastSubscriptions.map((sub) => ({\n ...sub,\n aggregatedQuotas: mergeLiveValues(sub.aggregatedQuotas, statusMap),\n })),\n [subscriptionsResult.pastSubscriptions, statusMap]\n );\n\n const refetch = useCallback(() => {\n dashboardResult.refetch();\n quotasSummaryResult.refetch();\n subscriptionsResult.refetch();\n }, [dashboardResult, quotasSummaryResult, subscriptionsResult]);\n\n return {\n dashboard: dashboardResult.dashboard,\n pooledQuotas,\n activeSubscriptions,\n pastSubscriptions,\n isLoading:\n dashboardResult.isLoading || quotasSummaryResult.isLoading || subscriptionsResult.isLoading,\n isPastSubscriptionsLoading: subscriptionsResult.isPastSubscriptionsLoading,\n error: dashboardResult.error || quotasSummaryResult.error || subscriptionsResult.error,\n refetch,\n // Pagination for past subscriptions\n loadMorePastSubscriptions: subscriptionsResult.loadMorePastSubscriptions,\n hasMorePastSubscriptions: subscriptionsResult.hasMorePastSubscriptions,\n isLoadingMorePast: subscriptionsResult.isLoadingMorePast,\n pastSubscriptionsCount: subscriptionsResult.pastSubscriptionsCount,\n };\n}\n"],"mappings":"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"}
|