@burdenoff/microfe-billing 2026.526.1 → 2026.528.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 +1 -1
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/BillingUserRoutes.js +1 -1
- package/dist/billing/BillingUserRoutes.js.map +1 -1
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js +15 -8
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js +13 -6
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonDetailPage.js +8 -4
- package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js +3 -2
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js +44 -23
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js +60 -31
- package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +9 -9
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js +8 -8
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js +16 -10
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +24 -16
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js +5 -1
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundsPage.js +46 -29
- package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/TransactionsPage.js +34 -22
- package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js +10 -10
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js +3 -3
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/NativeCheckoutView.js +7 -0
- package/dist/billing/modules/checkout/components/NativeCheckoutView.js.map +1 -1
- package/dist/billing/modules/checkout/components/OrderSummary.js +13 -13
- package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
- package/dist/billing/modules/checkout/components/PromoCodeInput.js +5 -5
- package/dist/billing/modules/checkout/components/PromoCodeInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js +5 -5
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js +4 -4
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js.map +1 -1
- package/dist/billing/modules/checkout/components/StripePaymentForm.js +11 -11
- package/dist/billing/modules/checkout/components/StripePaymentForm.js.map +1 -1
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js +3 -3
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js +8 -8
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js.map +1 -1
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +61 -52
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js +22 -15
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponsListPage.js +30 -21
- package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js +9 -8
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/EditCouponPage.js +11 -8
- package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js +22 -17
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js +15 -8
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditsPage.js +17 -11
- package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js +15 -11
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js +21 -17
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js +14 -11
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js +32 -26
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js +14 -10
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js +18 -11
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/pages/OverviewPage.js +44 -32
- package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js +35 -26
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/EarningsPage.js +15 -15
- package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +11 -11
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutsPage.js +5 -5
- package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +9 -4
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js +13 -8
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js +3 -2
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js +14 -7
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js +23 -9
- package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaPage.js +11 -7
- package/dist/billing/modules/quota/pages/QuotaPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js +3 -3
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js +6 -2
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js +5 -4
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js +26 -12
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/SettingsPage.js +356 -349
- package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js +157 -123
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js +13 -8
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +15 -7
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +30 -20
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js +25 -16
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js +50 -34
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/shared/components/AccessDenied.js +2 -2
- package/dist/billing/shared/components/AccessDenied.js.map +1 -1
- package/dist/billing/shared/components/ActorIdentity.js +67 -0
- package/dist/billing/shared/components/ActorIdentity.js.map +1 -0
- package/dist/billing/shared/components/CurrencyPriceOverridesEditor.js +6 -2
- package/dist/billing/shared/components/CurrencyPriceOverridesEditor.js.map +1 -1
- package/dist/billing/shared/components/EmptyState.js +7 -21
- package/dist/billing/shared/components/EmptyState.js.map +1 -1
- package/dist/billing/shared/components/PageHeader.js +7 -19
- package/dist/billing/shared/components/PageHeader.js.map +1 -1
- package/dist/billing/shared/components/ServerError.js +5 -4
- package/dist/billing/shared/components/ServerError.js.map +1 -1
- package/dist/billing/shared/components/index.js +1 -0
- package/dist/billing/shared/hooks/index.js +1 -0
- package/dist/billing/shared/hooks/useActorProfiles.js +113 -0
- package/dist/billing/shared/hooks/useActorProfiles.js.map +1 -0
- package/package.json +2 -2
|
@@ -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, getStatusBadgeClasses } from '../../../shared/utils/status';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\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=\"w-12 h-12 mx-auto rounded-full bg-muted flex items-center justify-center\">\n <svg\n className=\"w-6 h-6 text-muted-foreground\"\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-foreground\">Access Denied</h2>\n <p className=\"text-sm text-muted-foreground 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-muted animate-pulse rounded\" />\n <div className=\"space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border rounded-lg p-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"h-12 w-12 bg-muted animate-pulse rounded\" />\n <div className=\"flex-1 space-y-2\">\n <div className=\"h-5 w-32 bg-muted animate-pulse rounded\" />\n <div className=\"h-4 w-48 bg-muted 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=\"w-12 h-12 mx-auto rounded-full bg-destructive/10 flex items-center justify-center\">\n <svg\n className=\"w-6 h-6 text-destructive\"\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-foreground\">Failed to load subscriptions</h2>\n <p className=\"text-sm text-muted-foreground\">{error.message}</p>\n <button\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-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 font-bold text-foreground\">\n {tr('billing.subscriptions.title', 'Subscriptions')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.subscriptions.subtitle', 'Manage your active and past subscriptions')}\n </p>\n </div>\n\n {permissions.canCreateSubscription && (\n <button\n onClick={() => navigateTo('/plans')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n <svg className=\"w-4 h-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 onClick={() => setIsAccountSelectorOpen(!isAccountSelectorOpen)}\n className=\"w-full sm:w-auto flex items-center justify-between gap-3 px-4 py-3 border border-border rounded-lg bg-card hover:bg-muted/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center\">\n <svg\n className=\"w-4 h-4 text-primary\"\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-foreground\">\n {selectedBillingAccount?.name || 'Select Account'}\n </p>\n <p className=\"text-xs text-muted-foreground\">{selectedBillingAccount?.email}</p>\n </div>\n </div>\n <svg\n className={`w-4 h-4 text-muted-foreground 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-popover border border-border rounded-lg shadow-lg z-20 py-1 max-h-60 overflow-y-auto\">\n {billingAccounts.map((account) => (\n <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-muted transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"w-8 h-8 rounded-lg bg-muted flex items-center justify-center flex-shrink-0\">\n <svg\n className=\"w-4 h-4 text-muted-foreground\"\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-foreground truncate\">\n {account.name}\n </p>\n <p className=\"text-xs text-muted-foreground truncate\">{account.email}</p>\n </div>\n </div>\n {selectedBillingAccount?.id === account.id && (\n <svg\n className=\"w-4 h-4 text-primary 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 */}\n {selectedBillingAccount && (\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n <div className=\"border border-border rounded-lg bg-card p-4\">\n <p className=\"text-sm text-muted-foreground\">Account</p>\n <p className=\"text-lg font-semibold text-foreground mt-1\">\n {selectedBillingAccount.name}\n </p>\n </div>\n <div className=\"border border-border rounded-lg bg-card p-4\">\n <p className=\"text-sm text-muted-foreground\">Active Subscriptions</p>\n <p className=\"text-lg font-semibold text-status-success-text mt-1\">\n {activeSubscriptions.length}\n </p>\n </div>\n <div className=\"border border-border rounded-lg bg-card p-4\">\n <p className=\"text-sm text-muted-foreground\">Credit Balance</p>\n <p className=\"text-lg font-semibold text-foreground mt-1\">\n {(selectedBillingAccount.creditAmount || 0).toLocaleString()} Credits\n </p>\n </div>\n </div>\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\">\n {/* Status Filter */}\n <div className=\"inline-flex rounded-lg border border-border p-1 bg-muted/50 flex-wrap\">\n {(['all', 'active', 'canceled', 'inactive'] as FilterStatus[]).map((status) => (\n <button\n key={status}\n onClick={() => setFilterStatus(status)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors capitalize ${\n filterStatus === status\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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 w-4 h-4 text-muted-foreground\"\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 rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n </div>\n\n {/* Total Count */}\n <div className=\"text-sm text-muted-foreground\">\n {totalCount} subscription{totalCount !== 1 ? 's' : ''} total\n </div>\n </div>\n\n {/* Subscriptions List */}\n {filteredSubscriptions.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border rounded-lg\">\n <div className=\"w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-4\">\n <svg\n className=\"w-6 h-6 text-muted-foreground\"\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 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-foreground\">\n {tr('billing.subscriptions.noSubscriptionsFound', 'No subscriptions found')}\n </h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {filterStatus !== 'all' || debouncedSearch\n ? 'No subscriptions match your filters.'\n : \"You don't have any subscriptions yet.\"}\n </p>\n {permissions.canCreateSubscription && filterStatus === 'all' && !debouncedSearch && (\n <button\n onClick={() => navigateTo('/plans')}\n className=\"mt-4 px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Browse Plans\n </button>\n )}\n </div>\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 onClick={() => handlePageChange(currentPage - 1)}\n disabled={currentPage === 1}\n className=\"px-3 py-1.5 text-sm border border-border rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted transition-colors\"\n >\n <svg className=\"w-4 h-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-muted-foreground\">\n ...\n </span>\n );\n }\n\n if (!showPage) return null;\n\n return (\n <button\n key={page}\n onClick={() => handlePageChange(page)}\n className={`px-3 py-1.5 text-sm rounded-md transition-colors ${\n page === currentPage\n ? 'bg-primary text-primary-foreground'\n : 'border border-border hover:bg-muted'\n }`}\n >\n {page}\n </button>\n );\n })}\n </div>\n\n <button\n onClick={() => handlePageChange(currentPage + 1)}\n disabled={currentPage === totalPages}\n className=\"px-3 py-1.5 text-sm border border-border rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted transition-colors\"\n >\n <svg className=\"w-4 h-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 rounded-lg bg-card hover:shadow-md transition-shadow cursor-pointer\"\n onClick={onView}\n >\n <div className=\"p-6\">\n <div className=\"flex flex-col sm:flex-row sm:items-center gap-4\">\n {/* Plan Icon */}\n <div className=\"w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center shrink-0\">\n <svg\n className=\"w-6 h-6 text-primary\"\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-foreground\">{plan?.name || 'Unknown Plan'}</h3>\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded ${getStatusBadgeClasses(statusColor)}`}\n >\n {formatSubscriptionStatus(subscription.status)}\n </span>\n </div>\n <div className=\"flex items-center gap-4 mt-1 text-sm text-muted-foreground\">\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-foreground\">{daysRemaining} days</p>\n <p className=\"text-xs text-muted-foreground\">remaining</p>\n </div>\n )}\n <button\n onClick={(e) => {\n e.stopPropagation();\n onView();\n }}\n className=\"px-3 py-1.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\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\">\n <p className=\"text-sm text-muted-foreground\">\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\">\n <p className=\"text-sm text-muted-foreground\">\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, getStatusBadgeClasses } from '../../../shared/utils/status';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\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-muted flex items-center justify-center\">\n <svg\n className=\"size-6 text-muted-foreground\"\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-foreground\">Access Denied</h2>\n <p className=\"text-sm text-muted-foreground 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-muted animate-pulse rounded\" />\n <div className=\"space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border rounded-lg p-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-12 bg-muted animate-pulse rounded\" />\n <div className=\"flex-1 space-y-2\">\n <div className=\"h-5 w-32 bg-muted animate-pulse rounded\" />\n <div className=\"h-4 w-48 bg-muted 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-destructive/10 flex items-center justify-center\">\n <svg\n className=\"size-6 text-destructive\"\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-foreground\">Failed to load subscriptions</h2>\n <p className=\"text-sm text-muted-foreground\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-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 font-bold text-foreground\">\n {tr('billing.subscriptions.title', 'Subscriptions')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.subscriptions.subtitle', 'Manage your active and past subscriptions')}\n </p>\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-primary text-primary-foreground rounded-md hover:bg-primary/90 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=\"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 rounded-lg bg-card hover:bg-muted/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 rounded-lg bg-primary/10 flex items-center justify-center\">\n <svg\n className=\"size-4 text-primary\"\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-foreground\">\n {selectedBillingAccount?.name || 'Select Account'}\n </p>\n <p className=\"text-xs text-muted-foreground\">{selectedBillingAccount?.email}</p>\n </div>\n </div>\n <svg\n className={`size-4 text-muted-foreground 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-popover border border-border rounded-lg shadow-lg 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-muted transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"size-8 rounded-lg bg-muted flex items-center justify-center flex-shrink-0\">\n <svg\n className=\"size-4 text-muted-foreground\"\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-foreground truncate\">\n {account.name}\n </p>\n <p className=\"text-xs text-muted-foreground truncate\">{account.email}</p>\n </div>\n </div>\n {selectedBillingAccount?.id === account.id && (\n <svg\n className=\"size-4 text-primary 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 */}\n {selectedBillingAccount && (\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n <div className=\"border border-border rounded-lg bg-card p-4\">\n <p className=\"text-sm text-muted-foreground\">Account</p>\n <p className=\"text-lg font-semibold text-foreground mt-1\">\n {selectedBillingAccount.name}\n </p>\n </div>\n <div className=\"border border-border rounded-lg bg-card p-4\">\n <p className=\"text-sm text-muted-foreground\">Active Subscriptions</p>\n <p className=\"text-lg font-semibold text-status-success-text mt-1\">\n {activeSubscriptions.length}\n </p>\n </div>\n <div className=\"border border-border rounded-lg bg-card p-4\">\n <p className=\"text-sm text-muted-foreground\">Credit Balance</p>\n <p className=\"text-lg font-semibold text-foreground mt-1\">\n {(selectedBillingAccount.creditAmount || 0).toLocaleString()} Credits\n </p>\n </div>\n </div>\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\">\n {/* Status Filter */}\n <div className=\"inline-flex rounded-lg border border-border p-1 bg-muted/50 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-md transition-colors capitalize ${\n filterStatus === status\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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-muted-foreground\"\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 rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n </div>\n\n {/* Total Count */}\n <div className=\"text-sm text-muted-foreground\">\n {totalCount} subscription{totalCount !== 1 ? 's' : ''} total\n </div>\n </div>\n\n {/* Subscriptions List */}\n {filteredSubscriptions.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border rounded-lg\">\n <div className=\"size-12 rounded-full bg-muted flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-muted-foreground\"\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 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-foreground\">\n {tr('billing.subscriptions.noSubscriptionsFound', 'No subscriptions found')}\n </h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {filterStatus !== 'all' || debouncedSearch\n ? 'No subscriptions match your filters.'\n : \"You don't have any subscriptions yet.\"}\n </p>\n {permissions.canCreateSubscription && filterStatus === 'all' && !debouncedSearch && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"mt-4 px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Browse Plans\n </button>\n )}\n </div>\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 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted 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-muted-foreground\">\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-md transition-colors ${\n page === currentPage\n ? 'bg-primary text-primary-foreground'\n : 'border border-border hover:bg-muted'\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 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted 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 rounded-lg bg-card hover:shadow-md transition-shadow cursor-pointer\"\n onClick={onView}\n >\n <div className=\"p-6\">\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-primary/10 flex items-center justify-center shrink-0\">\n <svg\n className=\"size-6 text-primary\"\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-foreground\">{plan?.name || 'Unknown Plan'}</h3>\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded ${getStatusBadgeClasses(statusColor)}`}\n >\n {formatSubscriptionStatus(subscription.status)}\n </span>\n </div>\n <div className=\"flex items-center gap-4 mt-1 text-sm text-muted-foreground\">\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-foreground\">{daysRemaining} days</p>\n <p className=\"text-xs text-muted-foreground\">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 text-foreground rounded-md hover:bg-muted transition-colors\"\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\">\n <p className=\"text-sm text-muted-foreground\">\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\">\n <p className=\"text-sm text-muted-foreground\">\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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"}
|
|
@@ -46,18 +46,19 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
46
46
|
}) : /* @__PURE__ */ T("div", {
|
|
47
47
|
className: "mt-2",
|
|
48
48
|
children: [/* @__PURE__ */ T("button", {
|
|
49
|
+
type: "button",
|
|
49
50
|
onClick: (e) => {
|
|
50
51
|
e.stopPropagation(), n();
|
|
51
52
|
},
|
|
52
53
|
className: `flex items-center gap-1.5 text-xs font-medium transition-colors ${r === "current" ? "text-muted-foreground hover:text-foreground" : "text-primary/80 hover:text-primary"}`,
|
|
53
54
|
children: [
|
|
54
|
-
/* @__PURE__ */ w(x, { className: "
|
|
55
|
+
/* @__PURE__ */ w(x, { className: "size-3" }),
|
|
55
56
|
/* @__PURE__ */ T("span", { children: [
|
|
56
57
|
a,
|
|
57
58
|
" feature",
|
|
58
59
|
a === 1 ? "" : "s"
|
|
59
60
|
] }),
|
|
60
|
-
w(t ? _ : g, { className: "
|
|
61
|
+
w(t ? _ : g, { className: "size-3" })
|
|
61
62
|
]
|
|
62
63
|
}), t && /* @__PURE__ */ w("div", {
|
|
63
64
|
className: `mt-2 space-y-2 ${r === "current" ? "pl-0" : "pl-1"}`,
|
|
@@ -81,7 +82,7 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
81
82
|
children: [O(e), ":"]
|
|
82
83
|
}), /* @__PURE__ */ w("span", {
|
|
83
84
|
className: "font-medium text-foreground flex items-center gap-0.5",
|
|
84
|
-
children: t === -1 || t === Infinity ? /* @__PURE__ */ T(C, { children: [/* @__PURE__ */ w(v, { className: "
|
|
85
|
+
children: t === -1 || t === Infinity ? /* @__PURE__ */ T(C, { children: [/* @__PURE__ */ w(v, { className: "size-3" }), /* @__PURE__ */ w("span", {
|
|
85
86
|
className: "sr-only",
|
|
86
87
|
children: "Unlimited"
|
|
87
88
|
})] }) : D(t)
|
|
@@ -132,8 +133,8 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
132
133
|
className: "text-center space-y-2",
|
|
133
134
|
children: [
|
|
134
135
|
/* @__PURE__ */ w("div", {
|
|
135
|
-
className: "
|
|
136
|
-
children: /* @__PURE__ */ w(f, { className: "
|
|
136
|
+
className: "size-12 mx-auto rounded-full bg-muted flex items-center justify-center",
|
|
137
|
+
children: /* @__PURE__ */ w(f, { className: "size-6 text-muted-foreground" })
|
|
137
138
|
}),
|
|
138
139
|
/* @__PURE__ */ w("h2", {
|
|
139
140
|
className: "text-lg font-semibold text-foreground",
|
|
@@ -150,9 +151,9 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
150
151
|
className: "flex items-center justify-center min-h-[400px]",
|
|
151
152
|
children: /* @__PURE__ */ T("div", {
|
|
152
153
|
className: "text-center space-y-4",
|
|
153
|
-
children: [/* @__PURE__ */ w(y, { className: "
|
|
154
|
+
children: [/* @__PURE__ */ w(y, { className: "size-8 animate-spin mx-auto text-primary" }), /* @__PURE__ */ w("p", {
|
|
154
155
|
className: "text-muted-foreground",
|
|
155
|
-
children: "Loading plans
|
|
156
|
+
children: "Loading plans…"
|
|
156
157
|
})]
|
|
157
158
|
})
|
|
158
159
|
});
|
|
@@ -161,12 +162,13 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
161
162
|
children: /* @__PURE__ */ T("div", {
|
|
162
163
|
className: "text-center space-y-4",
|
|
163
164
|
children: [
|
|
164
|
-
/* @__PURE__ */ w(f, { className: "
|
|
165
|
+
/* @__PURE__ */ w(f, { className: "size-12 mx-auto text-destructive" }),
|
|
165
166
|
/* @__PURE__ */ w("h2", {
|
|
166
167
|
className: "text-lg font-semibold text-foreground",
|
|
167
168
|
children: "Subscription not found"
|
|
168
169
|
}),
|
|
169
170
|
/* @__PURE__ */ w("button", {
|
|
171
|
+
type: "button",
|
|
170
172
|
onClick: () => x(c("/subscriptions", M)),
|
|
171
173
|
className: "px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md",
|
|
172
174
|
children: _("billing.subscriptions.backToSubscriptions", "Back to Subscriptions")
|
|
@@ -177,13 +179,14 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
177
179
|
if (N.status !== "active") return /* @__PURE__ */ T("div", {
|
|
178
180
|
className: "max-w-2xl mx-auto p-6 space-y-6",
|
|
179
181
|
children: [/* @__PURE__ */ T("button", {
|
|
182
|
+
type: "button",
|
|
180
183
|
onClick: () => x(c(`/subscriptions/${v}`, M)),
|
|
181
184
|
className: "flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground",
|
|
182
|
-
children: [/* @__PURE__ */ w(p, { className: "
|
|
185
|
+
children: [/* @__PURE__ */ w(p, { className: "size-4" }), _("billing.subscriptions.backToSubscription", "Back to Subscription")]
|
|
183
186
|
}), /* @__PURE__ */ T("div", {
|
|
184
187
|
className: "text-center space-y-4 py-12",
|
|
185
188
|
children: [
|
|
186
|
-
/* @__PURE__ */ w(f, { className: "
|
|
189
|
+
/* @__PURE__ */ w(f, { className: "size-12 mx-auto text-status-warning-text" }),
|
|
187
190
|
/* @__PURE__ */ w("h2", {
|
|
188
191
|
className: "text-lg font-semibold text-foreground",
|
|
189
192
|
children: _("billing.subscriptions.cannotChangePlan", "Cannot Change Plan")
|
|
@@ -193,6 +196,7 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
193
196
|
children: _("billing.subscriptions.notActiveMessage", "This subscription is no longer active. You can only change plans for active subscriptions.")
|
|
194
197
|
}),
|
|
195
198
|
/* @__PURE__ */ w("button", {
|
|
199
|
+
type: "button",
|
|
196
200
|
onClick: () => x(c("/subscriptions", M)),
|
|
197
201
|
className: "px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md",
|
|
198
202
|
children: _("billing.subscriptions.viewAll", "View All Subscriptions")
|
|
@@ -203,13 +207,14 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
203
207
|
if (U.length === 0) return /* @__PURE__ */ T("div", {
|
|
204
208
|
className: "max-w-2xl mx-auto p-6 space-y-6",
|
|
205
209
|
children: [/* @__PURE__ */ T("button", {
|
|
210
|
+
type: "button",
|
|
206
211
|
onClick: () => x(c(`/subscriptions/${v}`, M)),
|
|
207
212
|
className: "flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground",
|
|
208
|
-
children: [/* @__PURE__ */ w(p, { className: "
|
|
213
|
+
children: [/* @__PURE__ */ w(p, { className: "size-4" }), _("billing.subscriptions.backToSubscription", "Back to Subscription")]
|
|
209
214
|
}), /* @__PURE__ */ T("div", {
|
|
210
215
|
className: "text-center space-y-4 py-12",
|
|
211
216
|
children: [
|
|
212
|
-
/* @__PURE__ */ w(b, { className: "
|
|
217
|
+
/* @__PURE__ */ w(b, { className: "size-12 mx-auto text-muted-foreground" }),
|
|
213
218
|
/* @__PURE__ */ w("h2", {
|
|
214
219
|
className: "text-lg font-semibold text-foreground",
|
|
215
220
|
children: _("billing.subscriptions.noPlansAvailable", "No Other Plans Available")
|
|
@@ -227,9 +232,10 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
227
232
|
children: [
|
|
228
233
|
/* @__PURE__ */ T("div", { children: [
|
|
229
234
|
/* @__PURE__ */ T("button", {
|
|
235
|
+
type: "button",
|
|
230
236
|
onClick: () => x(c(`/subscriptions/${v}`, M)),
|
|
231
237
|
className: "flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-4",
|
|
232
|
-
children: [/* @__PURE__ */ w(p, { className: "
|
|
238
|
+
children: [/* @__PURE__ */ w(p, { className: "size-4" }), _("billing.subscriptions.backToSubscription", "Back to Subscription")]
|
|
233
239
|
}),
|
|
234
240
|
/* @__PURE__ */ w("h1", {
|
|
235
241
|
className: "text-2xl font-bold text-foreground",
|
|
@@ -300,6 +306,7 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
300
306
|
return /* @__PURE__ */ T("div", {
|
|
301
307
|
className: `rounded-lg border transition-all ${i ? "border-primary bg-primary/5 ring-2 ring-primary/20" : "border-border bg-card hover:border-muted-foreground/30"}`,
|
|
302
308
|
children: [/* @__PURE__ */ w("button", {
|
|
309
|
+
type: "button",
|
|
303
310
|
onClick: () => R(e.id),
|
|
304
311
|
className: "w-full p-4 text-left",
|
|
305
312
|
children: /* @__PURE__ */ T("div", {
|
|
@@ -307,8 +314,8 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
307
314
|
children: [/* @__PURE__ */ T("div", {
|
|
308
315
|
className: "flex items-start gap-3",
|
|
309
316
|
children: [/* @__PURE__ */ w("div", {
|
|
310
|
-
className: `
|
|
311
|
-
children: i && /* @__PURE__ */ w(h, { className: "
|
|
317
|
+
className: `size-5 mt-0.5 rounded-full border-2 flex items-center justify-center shrink-0 ${i ? "border-primary bg-primary" : "border-muted-foreground/30"}`,
|
|
318
|
+
children: i && /* @__PURE__ */ w(h, { className: "size-3 text-primary-foreground" })
|
|
312
319
|
}), /* @__PURE__ */ T("div", {
|
|
313
320
|
className: "flex-1",
|
|
314
321
|
children: [/* @__PURE__ */ w("p", {
|
|
@@ -378,7 +385,7 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
378
385
|
})
|
|
379
386
|
]
|
|
380
387
|
}),
|
|
381
|
-
/* @__PURE__ */ w(m, { className: "
|
|
388
|
+
/* @__PURE__ */ w(m, { className: "size-5 text-muted-foreground" }),
|
|
382
389
|
/* @__PURE__ */ T("div", {
|
|
383
390
|
className: "text-center",
|
|
384
391
|
children: [
|
|
@@ -409,10 +416,12 @@ var D = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
409
416
|
/* @__PURE__ */ T("div", {
|
|
410
417
|
className: "flex items-center justify-end gap-3 pt-2 border-t border-border",
|
|
411
418
|
children: [/* @__PURE__ */ w("button", {
|
|
419
|
+
type: "button",
|
|
412
420
|
onClick: () => x(c(`/subscriptions/${v}`, M)),
|
|
413
421
|
className: "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground",
|
|
414
422
|
children: "Cancel"
|
|
415
423
|
}), /* @__PURE__ */ w("button", {
|
|
424
|
+
type: "button",
|
|
416
425
|
onClick: q,
|
|
417
426
|
className: "px-6 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors",
|
|
418
427
|
children: G ? "Downgrade Plan" : "Upgrade Plan"
|
|
@@ -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-dom';\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';\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-muted-foreground italic\">No features defined</p>;\n }\n\n return (\n <div className=\"mt-2\">\n <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-muted-foreground hover:text-foreground'\n : 'text-primary/80 hover:text-primary'\n }`}\n >\n <Sparkles className=\"h-3 w-3\" />\n <span>\n {featureCount} feature{featureCount !== 1 ? 's' : ''}\n </span>\n {isExpanded ? <ChevronUp className=\"h-3 w-3\" /> : <ChevronDown className=\"h-3 w-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-md p-2 ${\n variant === 'current' ? 'bg-muted/50' : 'bg-primary/5'\n }`}\n >\n <p\n className={`text-xs font-medium ${\n variant === 'current' ? 'text-foreground' : 'text-foreground'\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-muted-foreground\"\n >\n <span className=\"opacity-70\">{formatLimitKey(key)}:</span>\n <span className=\"font-medium text-foreground flex items-center gap-0.5\">\n {value === -1 || value === Infinity ? (\n <>\n <InfinityIcon className=\"h-3 w-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=\"w-12 h-12 mx-auto rounded-full bg-muted flex items-center justify-center\">\n <AlertCircle className=\"w-6 h-6 text-muted-foreground\" />\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">Access Denied</h2>\n <p className=\"text-sm text-muted-foreground 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=\"h-8 w-8 animate-spin mx-auto text-primary\" />\n <p className=\"text-muted-foreground\">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=\"w-12 h-12 mx-auto text-destructive\" />\n <h2 className=\"text-lg font-semibold text-foreground\">Subscription not found</h2>\n <button\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md\"\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 onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\n >\n <ArrowLeft className=\"h-4 w-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=\"w-12 h-12 mx-auto text-status-warning-text\" />\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.subscriptions.cannotChangePlan', 'Cannot Change Plan')}\n </h2>\n <p className=\"text-sm text-muted-foreground 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 onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md\"\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 onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\n >\n <ArrowLeft className=\"h-4 w-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=\"w-12 h-12 mx-auto text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.subscriptions.noPlansAvailable', 'No Other Plans Available')}\n </h2>\n <p className=\"text-sm text-muted-foreground 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 onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-4\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <h1 className=\"text-2xl font-bold text-foreground\">\n {tr('billing.subscriptions.changeYourPlan', 'Change Your Plan')}\n </h1>\n <p className=\"text-muted-foreground mt-1\">\n Switch from <span className=\"font-medium\">{subscription.plan?.name}</span> to a different\n plan\n </p>\n </div>\n\n {/* Current Plan Card */}\n <div className=\"bg-muted/30 border border-border rounded-lg p-4\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex-1\">\n <p className=\"text-sm text-muted-foreground\">Current Plan</p>\n <p className=\"font-semibold text-foreground\">{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-muted-foreground\">Price</p>\n <p className=\"font-semibold text-foreground\">\n {formatCurrency(subscription.plan?.price || 0, subscription.plan?.currency || 'USD')}\n <span className=\"text-sm font-normal text-muted-foreground\">\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-foreground\">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-primary bg-primary/5 ring-2 ring-primary/20'\n : 'border-border bg-card hover:border-muted-foreground/30'\n }`}\n >\n {/* Plan Header - Clickable for selection */}\n <button onClick={() => setSelectedPlanId(plan.id)} className=\"w-full p-4 text-left\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-3\">\n <div\n className={`w-5 h-5 mt-0.5 rounded-full border-2 flex items-center justify-center shrink-0 ${\n isSelected ? 'border-primary bg-primary' : 'border-muted-foreground/30'\n }`}\n >\n {isSelected && <CheckCircle className=\"w-3 h-3 text-primary-foreground\" />}\n </div>\n <div className=\"flex-1\">\n <p className=\"font-semibold text-foreground\">{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-foreground\">\n {formatCurrency(plan.price, plan.currency)}\n </p>\n <p className=\"text-sm text-muted-foreground\">\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-card border border-border 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-muted-foreground\">From</p>\n <p className=\"font-medium text-foreground\">{subscription.plan?.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {currentAggregatedCount} feature{currentAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n <ArrowRight className=\"h-5 w-5 text-muted-foreground\" />\n <div className=\"text-center\">\n <p className=\"text-xs text-muted-foreground\">To</p>\n <p className=\"font-medium text-primary\">{selectedPlan.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {selectedAggregatedCount} feature{selectedAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n\n {/* Info message */}\n <p className=\"text-sm text-muted-foreground 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\">\n <button\n onClick={() =>\n navigateTo(\n withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId)\n )\n }\n className=\"px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground\"\n >\n Cancel\n </button>\n <button\n onClick={handleProceedToCheckout}\n className=\"px-6 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 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-dom';\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';\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-muted-foreground 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-muted-foreground hover:text-foreground'\n : 'text-primary/80 hover:text-primary'\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-md p-2 ${\n variant === 'current' ? 'bg-muted/50' : 'bg-primary/5'\n }`}\n >\n <p\n className={`text-xs font-medium ${\n variant === 'current' ? 'text-foreground' : 'text-foreground'\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-muted-foreground\"\n >\n <span className=\"opacity-70\">{formatLimitKey(key)}:</span>\n <span className=\"font-medium text-foreground 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-muted flex items-center justify-center\">\n <AlertCircle className=\"size-6 text-muted-foreground\" />\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">Access Denied</h2>\n <p className=\"text-sm text-muted-foreground 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-primary\" />\n <p className=\"text-muted-foreground\">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-destructive\" />\n <h2 className=\"text-lg font-semibold text-foreground\">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-primary text-primary-foreground rounded-md\"\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-muted-foreground hover:text-foreground\"\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-foreground\">\n {tr('billing.subscriptions.cannotChangePlan', 'Cannot Change Plan')}\n </h2>\n <p className=\"text-sm text-muted-foreground 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-primary text-primary-foreground rounded-md\"\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-muted-foreground hover:text-foreground\"\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-muted-foreground\" />\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.subscriptions.noPlansAvailable', 'No Other Plans Available')}\n </h2>\n <p className=\"text-sm text-muted-foreground 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-muted-foreground hover:text-foreground mb-4\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <h1 className=\"text-2xl font-bold text-foreground\">\n {tr('billing.subscriptions.changeYourPlan', 'Change Your Plan')}\n </h1>\n <p className=\"text-muted-foreground mt-1\">\n Switch from <span className=\"font-medium\">{subscription.plan?.name}</span> to a different\n plan\n </p>\n </div>\n\n {/* Current Plan Card */}\n <div className=\"bg-muted/30 border border-border rounded-lg p-4\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex-1\">\n <p className=\"text-sm text-muted-foreground\">Current Plan</p>\n <p className=\"font-semibold text-foreground\">{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-muted-foreground\">Price</p>\n <p className=\"font-semibold text-foreground\">\n {formatCurrency(subscription.plan?.price || 0, subscription.plan?.currency || 'USD')}\n <span className=\"text-sm font-normal text-muted-foreground\">\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-foreground\">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-primary bg-primary/5 ring-2 ring-primary/20'\n : 'border-border bg-card hover:border-muted-foreground/30'\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 ? 'border-primary bg-primary' : 'border-muted-foreground/30'\n }`}\n >\n {isSelected && <CheckCircle className=\"size-3 text-primary-foreground\" />}\n </div>\n <div className=\"flex-1\">\n <p className=\"font-semibold text-foreground\">{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-foreground\">\n {formatCurrency(plan.price, plan.currency)}\n </p>\n <p className=\"text-sm text-muted-foreground\">\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-card border border-border 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-muted-foreground\">From</p>\n <p className=\"font-medium text-foreground\">{subscription.plan?.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {currentAggregatedCount} feature{currentAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n <ArrowRight className=\"size-5 text-muted-foreground\" />\n <div className=\"text-center\">\n <p className=\"text-xs text-muted-foreground\">To</p>\n <p className=\"font-medium text-primary\">{selectedPlan.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {selectedAggregatedCount} feature{selectedAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n\n {/* Info message */}\n <p className=\"text-sm text-muted-foreground 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\">\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-muted-foreground hover:text-foreground\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleProceedToCheckout}\n className=\"px-6 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 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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"}
|