@burdenoff/microfe-billing 2026.828.2 → 2026.828.4
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/hooks/useProducts.js +14 -0
- package/dist/billing/hooks/useProducts.js.map +1 -0
- package/dist/billing/modules/addons/pages/AddonCreatePage.js +159 -151
- package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +18 -3
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/shared/types/graphql.js.map +1 -1
- package/dist/generated/global-operations.js +20 -1
- package/dist/generated/global-operations.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddonCreatePage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonCreatePage.tsx"],"sourcesContent":["/**\n * Addons Module - Addon Create Page\n * Form to create a new addon\n */\n\nimport { useState, type FC, type FormEvent } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useAddonMutations } from '../hooks';\nimport { useQuotas } from '../../plans/hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, CurrencyPriceOverridesEditor, PageHeader } from '../../../shared/components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PlanDuration,\n type CreateAddonInput,\n type CurrencyPriceOverrideInput,\n} from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface QuotaAllocation {\n quotaId: string;\n quantity: number;\n}\n\nexport const AddonCreatePage: 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 { quotas, isLoading: isQuotasLoading } = useQuotas({ onlyActive: true });\n const { createAddon, isCreating } = useAddonMutations();\n\n // Form state\n const [name, setName] = useState('');\n const [price, setPrice] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [duration, setDuration] = useState<PlanDuration>(PlanDuration.MONTHLY);\n const [productId, setProductId] = useState('');\n const [quotaAllocations, setQuotaAllocations] = useState<QuotaAllocation[]>([]);\n const [currencyPriceOverrides, setCurrencyPriceOverrides] = useState<\n CurrencyPriceOverrideInput[]\n >([]);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Permission check\n if (!permissions.canManageAddons) {\n return (\n <AccessDenied\n message={tr(\n 'billing.addons.noCreatePermission',\n \"You don't have permission to create addons.\"\n )}\n />\n );\n }\n\n // Loading state\n if (isQuotasLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"max-w-2xl space-y-4\">\n {[1, 2, 3, 4, 5].map((i) => (\n <div key={i} className=\"h-16 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n const handleAddQuota = () => {\n // Add new quota at the top with empty quotaId (placeholder)\n setQuotaAllocations([{ quotaId: '', quantity: 1 }, ...quotaAllocations]);\n };\n\n const handleRemoveQuota = (index: number) => {\n setQuotaAllocations(quotaAllocations.filter((_, i) => i !== index));\n };\n\n const handleQuotaChange = (\n index: number,\n field: 'quotaId' | 'quantity',\n value: string | number\n ) => {\n const updated = [...quotaAllocations];\n if (field === 'quotaId') {\n updated[index].quotaId = value as string;\n } else {\n updated[index].quantity = Number(value);\n }\n setQuotaAllocations(updated);\n };\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n setSuccess(false);\n\n // Validation\n if (!name.trim()) {\n setError('Addon name is required');\n return;\n }\n if (!price || isNaN(Number(price)) || Number(price) < 0) {\n setError('Valid price is required');\n return;\n }\n if (!productId.trim()) {\n setError('Product ID is required');\n return;\n }\n if (quotaAllocations.length === 0) {\n setError('At least one quota is required');\n return;\n }\n // Check for unselected quotas\n const unselectedQuotas = quotaAllocations.filter((a) => a.quotaId === '');\n if (unselectedQuotas.length > 0) {\n setError('Please select a quota for all allocations');\n return;\n }\n\n const input: CreateAddonInput = {\n name: name.trim(),\n price: Number(price),\n currency,\n duration,\n productId: productId.trim(),\n quotas: quotaAllocations.map((a) => ({\n quotaId: a.quotaId,\n quantity: a.quantity,\n })),\n ...(currencyPriceOverrides.length > 0 ? { currencyPriceOverrides } : {}),\n };\n\n try {\n await createAddon(input);\n setSuccess(true);\n // Navigate back after short delay\n setTimeout(() => navigateTo('/addons'), 1500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create addon');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={tr('billing.addons.createNewAddon', 'Create New Addon')}\n description={tr('billing.addons.createNewAddonDesc', 'Set up a new addon with quotas')}\n />\n\n <PagePurpose>\n {tr(\n 'billing.addons.createPurpose',\n 'Create an add-on that customers can buy on top of a subscription — extra seats, storage, usage credits and so on. Set its price (with optional per-currency overrides) and the quotas it grants, then make it available to purchase.'\n )}\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Error/Success Messages */}\n {error && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n {success && (\n <div className=\"p-4 bg-status-success-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-success-text\">\n {tr('billing.addons.createSuccess', 'Addon created successfully! Redirecting...')}\n </p>\n </div>\n )}\n\n {/* Two-column layout on desktop */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n {/* Basic Info */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)] h-fit\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.basicInfo', 'Basic Information')}\n </h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Name */}\n <div>\n <label htmlFor=\"name\" className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('billing.addons.addonNameLabel', 'Addon Name')} *\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder={tr('billing.addons.addonNamePlaceholder', 'e.g., Extra Storage')}\n required\n />\n </div>\n\n {/* Product ID */}\n <div>\n <label\n htmlFor=\"productId\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.addons.productId', 'Product ID')} *\n </label>\n <input\n id=\"productId\"\n type=\"text\"\n value={productId}\n onChange={(e) => setProductId(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder={tr('billing.addons.productIdPlaceholder', 'e.g., prod_abc123')}\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n {tr('billing.addons.productIdDesc', 'The product this addon belongs to')}\n </p>\n </div>\n\n {/* Price & Currency */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"price\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.addons.priceLabel', 'Price')} *\n </label>\n <input\n id=\"price\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={price}\n onChange={(e) => setPrice(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"9.99\"\n required\n />\n </div>\n <div>\n <label\n htmlFor=\"currency\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.addons.currencyLabel', 'Currency')} *\n </label>\n <select\n id=\"currency\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"USD\">USD ($)</option>\n <option value=\"EUR\">EUR</option>\n <option value=\"GBP\">GBP</option>\n <option value=\"INR\">INR</option>\n <option value=\"AUD\">AUD</option>\n <option value=\"CAD\">CAD</option>\n <option value=\"JPY\">JPY</option>\n <option value=\"SGD\">SGD</option>\n </select>\n </div>\n </div>\n\n {/* Duration */}\n <div>\n <label\n htmlFor=\"duration\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.addons.billingPeriodLabel', 'Billing Period')} *\n </label>\n <select\n id=\"duration\"\n value={duration}\n onChange={(e) => setDuration(e.target.value as PlanDuration)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value={PlanDuration.MONTHLY}>Monthly</option>\n <option value={PlanDuration.YEARLY}>Yearly</option>\n </select>\n </div>\n </div>\n </div>\n\n {/* Quota Allocations */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)] h-fit\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30 flex items-center justify-between\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.quotaAllocations', 'Quota Allocations')} *\n </h2>\n <button\n type=\"button\"\n onClick={handleAddQuota}\n disabled={quotas.length === 0}\n className=\"px-3 py-1 text-sm font-medium text-text-link hover:bg-[var(--color-accent-soft)] rounded-md transition-colors disabled:opacity-50\"\n >\n + Add Quota\n </button>\n </div>\n <div className=\"p-4 space-y-3\">\n {quotas.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'billing.addons.noQuotasAvailable',\n 'No quotas available. Please create quotas first.'\n )}\n </p>\n ) : quotaAllocations.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'billing.addons.noQuotasAllocated',\n 'No quotas allocated. Click \"Add Quota\" to add one.'\n )}\n </p>\n ) : (\n quotaAllocations.map((allocation, index) => {\n const selectedQuota = quotas.find((q) => q.id === allocation.quotaId);\n const limits = selectedQuota?.limits as { type?: string; value?: number } | null;\n const limitValue = limits?.value ?? 0;\n const limitType = limits?.type ?? 'unknown';\n const totalValue = limitValue * allocation.quantity;\n\n return (\n <div\n key={index}\n className=\"p-3 border border-border-subtle rounded-lg bg-bg-sunken/20 space-y-3\"\n >\n <div className=\"flex items-center gap-3\">\n <select\n value={allocation.quotaId}\n onChange={(e) => handleQuotaChange(index, 'quotaId', e.target.value)}\n className={`flex-1 px-3 py-2 border border-border-subtle rounded-input bg-bg-surface focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] ${\n allocation.quotaId === '' ? 'text-text-muted' : 'text-text-primary'\n }`}\n >\n <option value=\"\" disabled>\n {tr('billing.addons.selectQuota', 'Select a quota...')}\n </option>\n {quotas.map((quota) => (\n <option key={quota.id} value={quota.id}>\n {quota.name}\n </option>\n ))}\n </select>\n <button\n type=\"button\"\n onClick={() => handleRemoveQuota(index)}\n className=\"p-2 text-status-error-text hover:bg-status-error-bg-subtle rounded-md transition-colors\"\n title=\"Remove quota\"\n >\n <svg\n className=\"size-5\"\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 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\"\n />\n </svg>\n </button>\n </div>\n\n {/* Quota Info - only show when quota is selected */}\n {selectedQuota && (\n <>\n <div className=\"flex items-center justify-between text-sm\">\n <div className=\"text-text-muted\">\n <span className=\"capitalize\">Type: {limitType}</span>\n <span className=\"mx-2\">•</span>\n <span>\n Value per unit:{' '}\n <span className=\"font-medium text-text-primary\">\n {limitValue.toLocaleString()}\n </span>\n </span>\n </div>\n </div>\n\n {/* Quantity & Total */}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex items-center gap-2\">\n <label className=\"text-sm text-text-muted\">\n {tr('billing.addons.quantity', 'Quantity')}:\n </label>\n <input\n type=\"number\"\n min=\"1\"\n value={allocation.quantity}\n onChange={(e) =>\n handleQuotaChange(index, 'quantity', e.target.value)\n }\n className=\"w-24 px-3 py-1.5 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] text-center\"\n />\n </div>\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-text-muted\">=</span>\n <span className=\"font-semibold text-status-success-text\">\n {totalValue.toLocaleString()} total\n </span>\n </div>\n </div>\n </>\n )}\n </div>\n );\n })\n )}\n </div>\n </div>\n </div>\n\n <CurrencyPriceOverridesEditor\n overrides={currencyPriceOverrides}\n onChange={setCurrencyPriceOverrides}\n />\n\n {/* Actions */}\n <div className=\"flex items-center gap-3 pt-4\">\n <button\n type=\"submit\"\n disabled={isCreating || quotas.length === 0}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors disabled:opacity-50\"\n >\n {isCreating\n ? tr('billing.addons.creating', 'Creating...')\n : tr('billing.addons.createAddonBtn', 'Create Addon')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons')}\n className=\"px-6 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Cancel\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AddonCreatePage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonCreatePage.tsx"],"sourcesContent":["/**\n * Addons Module - Addon Create Page\n * Form to create a new addon\n */\n\nimport { useState, type FC, type FormEvent } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useAddonMutations } from '../hooks';\nimport { useQuotas } from '../../plans/hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useProducts } from '../../../hooks/useProducts';\nimport { AccessDenied, CurrencyPriceOverridesEditor, PageHeader } from '../../../shared/components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PlanDuration,\n type CreateAddonInput,\n type CurrencyPriceOverrideInput,\n} from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface QuotaAllocation {\n quotaId: string;\n quantity: number;\n}\n\nexport const AddonCreatePage: 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 { quotas, isLoading: isQuotasLoading } = useQuotas({ onlyActive: true });\n const { products, isLoading: isProductsLoading } = useProducts();\n const { createAddon, isCreating } = useAddonMutations();\n\n // Form state\n const [name, setName] = useState('');\n const [price, setPrice] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [duration, setDuration] = useState<PlanDuration>(PlanDuration.MONTHLY);\n const [productId, setProductId] = useState('');\n const [quotaAllocations, setQuotaAllocations] = useState<QuotaAllocation[]>([]);\n const [currencyPriceOverrides, setCurrencyPriceOverrides] = useState<\n CurrencyPriceOverrideInput[]\n >([]);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Permission check\n if (!permissions.canManageAddons) {\n return (\n <AccessDenied\n message={tr(\n 'billing.addons.noCreatePermission',\n \"You don't have permission to create addons.\"\n )}\n />\n );\n }\n\n // Loading state\n if (isQuotasLoading || isProductsLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"max-w-2xl space-y-4\">\n {[1, 2, 3, 4, 5].map((i) => (\n <div key={i} className=\"h-16 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n const handleAddQuota = () => {\n // Add new quota at the top with empty quotaId (placeholder)\n setQuotaAllocations([{ quotaId: '', quantity: 1 }, ...quotaAllocations]);\n };\n\n const handleRemoveQuota = (index: number) => {\n setQuotaAllocations(quotaAllocations.filter((_, i) => i !== index));\n };\n\n const handleQuotaChange = (\n index: number,\n field: 'quotaId' | 'quantity',\n value: string | number\n ) => {\n const updated = [...quotaAllocations];\n if (field === 'quotaId') {\n updated[index].quotaId = value as string;\n } else {\n updated[index].quantity = Number(value);\n }\n setQuotaAllocations(updated);\n };\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n setSuccess(false);\n\n // Validation\n if (!name.trim()) {\n setError('Addon name is required');\n return;\n }\n if (!price || isNaN(Number(price)) || Number(price) < 0) {\n setError('Valid price is required');\n return;\n }\n if (!productId.trim()) {\n setError('Product ID is required');\n return;\n }\n if (quotaAllocations.length === 0) {\n setError('At least one quota is required');\n return;\n }\n // Check for unselected quotas\n const unselectedQuotas = quotaAllocations.filter((a) => a.quotaId === '');\n if (unselectedQuotas.length > 0) {\n setError('Please select a quota for all allocations');\n return;\n }\n\n const input: CreateAddonInput = {\n name: name.trim(),\n price: Number(price),\n currency,\n duration,\n productId: productId.trim(),\n quotas: quotaAllocations.map((a) => ({\n quotaId: a.quotaId,\n quantity: a.quantity,\n })),\n ...(currencyPriceOverrides.length > 0 ? { currencyPriceOverrides } : {}),\n };\n\n try {\n await createAddon(input);\n setSuccess(true);\n // Navigate back after short delay\n setTimeout(() => navigateTo('/addons'), 1500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create addon');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={tr('billing.addons.createNewAddon', 'Create New Addon')}\n description={tr('billing.addons.createNewAddonDesc', 'Set up a new addon with quotas')}\n />\n\n <PagePurpose>\n {tr(\n 'billing.addons.createPurpose',\n 'Create an add-on that customers can buy on top of a subscription — extra seats, storage, usage credits and so on. Set its price (with optional per-currency overrides) and the quotas it grants, then make it available to purchase.'\n )}\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Error/Success Messages */}\n {error && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n {success && (\n <div className=\"p-4 bg-status-success-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-success-text\">\n {tr('billing.addons.createSuccess', 'Addon created successfully! Redirecting...')}\n </p>\n </div>\n )}\n\n {/* Two-column layout on desktop */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n {/* Basic Info */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)] h-fit\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.basicInfo', 'Basic Information')}\n </h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Name */}\n <div>\n <label htmlFor=\"name\" className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('billing.addons.addonNameLabel', 'Addon Name')} *\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder={tr('billing.addons.addonNamePlaceholder', 'e.g., Extra Storage')}\n required\n />\n </div>\n\n {/* Product */}\n <div>\n <label\n htmlFor=\"productId\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.addons.product', 'Product')} *\n </label>\n <select\n id=\"productId\"\n value={productId}\n onChange={(e) => setProductId(e.target.value)}\n disabled={products.length === 0}\n className={`w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50 ${\n productId === '' ? 'text-text-muted' : 'text-text-primary'\n }`}\n required\n >\n <option value=\"\" disabled>\n {tr('billing.addons.selectProduct', 'Select a product...')}\n </option>\n {products.map((product) => (\n <option key={product.id} value={product.id}>\n {product.name}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-text-muted mt-1\">\n {products.length === 0\n ? tr('billing.addons.noProductsAvailable', 'No active products found.')\n : tr('billing.addons.productIdDesc', 'The product this addon belongs to')}\n </p>\n </div>\n\n {/* Price & Currency */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"price\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.addons.priceLabel', 'Price')} *\n </label>\n <input\n id=\"price\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={price}\n onChange={(e) => setPrice(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"9.99\"\n required\n />\n </div>\n <div>\n <label\n htmlFor=\"currency\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.addons.currencyLabel', 'Currency')} *\n </label>\n <select\n id=\"currency\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"USD\">USD ($)</option>\n <option value=\"EUR\">EUR</option>\n <option value=\"GBP\">GBP</option>\n <option value=\"INR\">INR</option>\n <option value=\"AUD\">AUD</option>\n <option value=\"CAD\">CAD</option>\n <option value=\"JPY\">JPY</option>\n <option value=\"SGD\">SGD</option>\n </select>\n </div>\n </div>\n\n {/* Duration */}\n <div>\n <label\n htmlFor=\"duration\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.addons.billingPeriodLabel', 'Billing Period')} *\n </label>\n <select\n id=\"duration\"\n value={duration}\n onChange={(e) => setDuration(e.target.value as PlanDuration)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value={PlanDuration.MONTHLY}>Monthly</option>\n <option value={PlanDuration.YEARLY}>Yearly</option>\n </select>\n </div>\n </div>\n </div>\n\n {/* Quota Allocations */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)] h-fit\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30 flex items-center justify-between\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.quotaAllocations', 'Quota Allocations')} *\n </h2>\n <button\n type=\"button\"\n onClick={handleAddQuota}\n disabled={quotas.length === 0}\n className=\"px-3 py-1 text-sm font-medium text-text-link hover:bg-[var(--color-accent-soft)] rounded-md transition-colors disabled:opacity-50\"\n >\n + Add Quota\n </button>\n </div>\n <div className=\"p-4 space-y-3\">\n {quotas.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'billing.addons.noQuotasAvailable',\n 'No quotas available. Please create quotas first.'\n )}\n </p>\n ) : quotaAllocations.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'billing.addons.noQuotasAllocated',\n 'No quotas allocated. Click \"Add Quota\" to add one.'\n )}\n </p>\n ) : (\n quotaAllocations.map((allocation, index) => {\n const selectedQuota = quotas.find((q) => q.id === allocation.quotaId);\n const limits = selectedQuota?.limits as { type?: string; value?: number } | null;\n const limitValue = limits?.value ?? 0;\n const limitType = limits?.type ?? 'unknown';\n const totalValue = limitValue * allocation.quantity;\n\n return (\n <div\n key={index}\n className=\"p-3 border border-border-subtle rounded-lg bg-bg-sunken/20 space-y-3\"\n >\n <div className=\"flex items-center gap-3\">\n <select\n value={allocation.quotaId}\n onChange={(e) => handleQuotaChange(index, 'quotaId', e.target.value)}\n className={`flex-1 px-3 py-2 border border-border-subtle rounded-input bg-bg-surface focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] ${\n allocation.quotaId === '' ? 'text-text-muted' : 'text-text-primary'\n }`}\n >\n <option value=\"\" disabled>\n {tr('billing.addons.selectQuota', 'Select a quota...')}\n </option>\n {quotas.map((quota) => (\n <option key={quota.id} value={quota.id}>\n {quota.name}\n </option>\n ))}\n </select>\n <button\n type=\"button\"\n onClick={() => handleRemoveQuota(index)}\n className=\"p-2 text-status-error-text hover:bg-status-error-bg-subtle rounded-md transition-colors\"\n title=\"Remove quota\"\n >\n <svg\n className=\"size-5\"\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 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\"\n />\n </svg>\n </button>\n </div>\n\n {/* Quota Info - only show when quota is selected */}\n {selectedQuota && (\n <>\n <div className=\"flex items-center justify-between text-sm\">\n <div className=\"text-text-muted\">\n <span className=\"capitalize\">Type: {limitType}</span>\n <span className=\"mx-2\">•</span>\n <span>\n Value per unit:{' '}\n <span className=\"font-medium text-text-primary\">\n {limitValue.toLocaleString()}\n </span>\n </span>\n </div>\n </div>\n\n {/* Quantity & Total */}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex items-center gap-2\">\n <label className=\"text-sm text-text-muted\">\n {tr('billing.addons.quantity', 'Quantity')}:\n </label>\n <input\n type=\"number\"\n min=\"1\"\n value={allocation.quantity}\n onChange={(e) =>\n handleQuotaChange(index, 'quantity', e.target.value)\n }\n className=\"w-24 px-3 py-1.5 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] text-center\"\n />\n </div>\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-text-muted\">=</span>\n <span className=\"font-semibold text-status-success-text\">\n {totalValue.toLocaleString()} total\n </span>\n </div>\n </div>\n </>\n )}\n </div>\n );\n })\n )}\n </div>\n </div>\n </div>\n\n <CurrencyPriceOverridesEditor\n overrides={currencyPriceOverrides}\n onChange={setCurrencyPriceOverrides}\n />\n\n {/* Actions */}\n <div className=\"flex items-center gap-3 pt-4\">\n <button\n type=\"submit\"\n disabled={isCreating || quotas.length === 0}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors disabled:opacity-50\"\n >\n {isCreating\n ? tr('billing.addons.creating', 'Creating...')\n : tr('billing.addons.createAddonBtn', 'Create Addon')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons')}\n className=\"px-6 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Cancel\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -28,7 +28,8 @@ function x(e) {
|
|
|
28
28
|
quotaType: n.quota.quotaType ?? "",
|
|
29
29
|
limitType: r,
|
|
30
30
|
totalValue: i,
|
|
31
|
-
unit: s(n)
|
|
31
|
+
unit: s(n),
|
|
32
|
+
perWorkspace: n.quota.scope === "WORKSPACE"
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
return Array.from(t.values());
|
|
@@ -184,9 +185,12 @@ var S = () => {
|
|
|
184
185
|
children: [/* @__PURE__ */ m("p", {
|
|
185
186
|
className: "font-medium text-text-primary",
|
|
186
187
|
children: e.name
|
|
187
|
-
}), /* @__PURE__ */
|
|
188
|
+
}), /* @__PURE__ */ h("span", {
|
|
188
189
|
className: "font-semibold text-text-primary",
|
|
189
|
-
children: i(e.totalValue, e.unit)
|
|
190
|
+
children: [i(e.totalValue, e.unit), e.perWorkspace && /* @__PURE__ */ h("span", {
|
|
191
|
+
className: "font-normal text-text-secondary",
|
|
192
|
+
children: [" ", s("billing.plans.perWorkspace", "per workspace")]
|
|
193
|
+
})]
|
|
190
194
|
})]
|
|
191
195
|
}), /* @__PURE__ */ h("div", {
|
|
192
196
|
className: "flex items-center gap-2 mt-1",
|
|
@@ -235,6 +239,17 @@ var S = () => {
|
|
|
235
239
|
className: "text-sm text-text-primary mt-0.5",
|
|
236
240
|
children: r(y.duration)
|
|
237
241
|
})] }),
|
|
242
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
243
|
+
className: "text-sm text-text-secondary",
|
|
244
|
+
children: s("billing.plans.maxWorkspaces", "Max Workspaces")
|
|
245
|
+
}), /* @__PURE__ */ h("p", {
|
|
246
|
+
className: "text-sm font-medium text-text-primary mt-0.5",
|
|
247
|
+
children: [
|
|
248
|
+
y.maxWorkspaces ?? 1,
|
|
249
|
+
" ",
|
|
250
|
+
(y.maxWorkspaces ?? 1) === 1 ? s("billing.plans.workspaceSingular", "workspace") : s("billing.plans.workspacePlural", "workspaces")
|
|
251
|
+
]
|
|
252
|
+
})] }),
|
|
238
253
|
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
239
254
|
className: "text-sm text-text-secondary",
|
|
240
255
|
children: s("billing.plans.quotasIncluded", "Quotas Included")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlanBrowseDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanBrowseDetailPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Browse Detail Page (User-facing)\n * View plan details and subscribe. No edit/toggle actions.\n */\n\nimport { type FC, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n formatQuotaValue,\n getPlanFeatureQuantity,\n getPlanFeatureUnit,\n getQuotaDisplayName,\n isServerError,\n} from '../../../shared/utils';\nimport type { PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ShowMore, PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport type { ReactNode } from 'react';\n\n/** Wraps children in a ShowMore progressive-disclosure region only when `when` is true. */\nconst ShowMoreIf: FC<{ when: boolean; collapsedHeight?: number; children: ReactNode }> = ({\n when,\n collapsedHeight,\n children,\n}) => (when ? <ShowMore collapsedHeight={collapsedHeight}>{children}</ShowMore> : <>{children}</>);\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n quotaType: string;\n limitType: string;\n totalValue: number;\n unit?: string;\n}\n\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limits = feature.quota.limits as { type?: string; value?: number } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: getQuotaDisplayName(feature),\n quotaType: feature.quota.quotaType ?? '',\n limitType,\n totalValue: limitValue,\n unit: getPlanFeatureUnit(feature),\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const PlanBrowseDetailPage: 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 { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const { plan, isLoading, error, refetch } = usePlan(planId);\n\n const combinedQuotas = useMemo(() => combineQuotas(plan?.features || []), [plan?.features]);\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-10 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.plans.unableToLoadPlanDetails', 'Unable to load plan details.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!plan) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.planNotFound', 'Plan not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {error?.message ||\n tr(\n 'billing.plans.planNotFoundMessage',\n 'The plan you are looking for does not exist.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <svg className=\"size-5\" 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 <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan.name}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {formatPlanDurationLabel(plan.duration)} billing plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.plans.browseDetailPurpose',\n 'Everything this plan includes — its price, billing period and the full list of features and quotas — so you can confirm it fits before subscribing. Hit Subscribe when you are ready to check out.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Pricing Card — primary emphasis zone */}\n <EmphasisPanel className=\"overflow-hidden p-0\">\n <div className=\"p-6 border-b border-border-seam\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.pricing', 'Pricing')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </span>\n <span className=\"text-lg text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {tr('billing.plans.billed', 'Billed')}{' '}\n {plan.duration === 'monthly'\n ? tr('billing.plans.everyMonth', 'every month')\n : tr('billing.plans.annually', 'annually')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Features Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.featuresAndQuotas', 'Features & Quotas')} (\n {combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-6\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.noFeatures', 'No features configured for this plan.')}\n </p>\n ) : (\n <ShowMoreIf when={combinedQuotas.length > 4} collapsedHeight={280}>\n <ul className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <li\n key={quota.quotaId}\n className=\"flex items-start gap-3 p-3 rounded-lg bg-bg-sunken\"\n >\n <div className=\"size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0\">\n <svg\n className=\"size-4 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <span className=\"font-semibold text-text-primary\">\n {formatQuotaValue(quota.totalValue, quota.unit)}\n </span>\n </div>\n <div className=\"flex items-center gap-2 mt-1\">\n <span className=\"text-sm text-text-secondary capitalize\">\n Type: {quota.limitType}\n </span>\n {quota.quotaType && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-secondary rounded\">\n {quota.quotaType}\n </span>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n </ShowMoreIf>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6 lg:sticky lg:top-6 lg:self-start\">\n {/* Subscribe Card — primary emphasis zone */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)] overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.subscribeTitle', 'Subscribe')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">{tr('billing.plans.price', 'Price')}</p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {formatCurrency(plan.price, plan.currency)} / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.billingPeriod', 'Billing Period')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatPlanDurationLabel(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.quotasIncluded', 'Quotas Included')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {combinedQuotas.length}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"w-full px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n </div>\n\n {/* Back */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AA0BA,IAAM,KAAoF,EACxF,SACA,oBACA,kBACK,IAAO,kBAAC,GAAD;CAA2B;CAAkB;AAAmB,CAAA,IAAI,kBAAA,GAAA,EAAG,YAAW,CAAA;AAWhG,SAAS,EAAc,GAA0C;CAC/D,IAAM,oBAAW,IAAI,IAA2B;CAChD,KAAK,IAAM,KAAW,GAAU;EAC9B,IAAI,CAAC,EAAQ,OAAO;EACpB,IAAM,IAAU,EAAQ,MAAM,IAExB,IADS,EAAQ,MAAM,QACH,QAAQ,WAC5B,IAAa,EAAuB,CAAO,GAC3C,IAAW,EAAS,IAAI,CAAO;EACrC,AAAI,IACF,EAAS,cAAc,IAEvB,EAAS,IAAI,GAAS;GACpB;GACA,MAAM,EAAoB,CAAO;GACjC,WAAW,EAAQ,MAAM,aAAa;GACtC;GACA,YAAY;GACZ,MAAM,EAAmB,CAAO;EAClC,CAAC;CAEL;CACA,OAAO,MAAM,KAAK,EAAS,OAAO,CAAC;AACrC;AAEA,IAAa,UAAiC;CAC5C,IAAM,EAAE,SAAM,EAAQ,GAChB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,CAAG;EACxB,OAAO,MAAe,IAAM,IAAW;CACzC,GACM,EAAE,cAAW,EAA8B,GAC3C,IAAa,EAAmB,GAChC,EAAE,SAAM,cAAW,UAAO,eAAY,EAAQ,CAAM,GAEpD,IAAiB,QAAc,EAAc,GAAM,YAAY,CAAC,CAAC,GAAG,CAAC,GAAM,QAAQ,CAAC;CAgE1F,OA9DI,IAEA,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAK,WAAU,6CAA8C,CAAA,GAC7D,kBAAC,OAAD,EAAK,WAAU,8CAA+C,CAAA,CAC3D;MACL,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAK,WAAU,8CAA+C,CAAA,GAC9D,kBAAC,OAAD,EAAK,WAAU,gDAAiD,CAAA,CAC7D;;GACF,CAAA,GACL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAK,WAAU,8CAA+C,CAAA,GAC9D,kBAAC,OAAD,EAAK,WAAU,iDAAkD,CAAA,CAC9D;KACF;IACF;MAIL,KAAS,EAAc,CAAK,IAE5B,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,OAAO,EAAG,mCAAmC,oBAAoB;GACjE,SAAS,EAAG,yCAAyC,8BAA8B;GACnF,eAAe,EAAQ;GACvB,WAAA;EACD,CAAA;CACE,CAAA,IAIJ,IA2BH,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,QAAQ;KAClC,WAAU;eAEV,kBAAC,OAAD;MAAK,WAAU;MAAS,MAAK;MAAO,SAAQ;MAAY,QAAO;gBAC7D,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;MACH,CAAA;KACE,CAAA;IACC,CAAA,GACR,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAK;KACJ,CAAA;KACJ,kBAAC,KAAD;MAAG,WAAU;gBAAb,CACG,EAAwB,EAAK,QAAQ,GAAE,eACvC;;KACH,kBAAC,GAAD;MAAa,WAAU;gBACpB,EACC,qCACA,oMACF;KACW,CAAA;IACV,EAAA,CAAA,CACF;OAEL,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,aAAa,EAAK,IAAI;IAChD,WAAU;cACX;GAEO,CAAA,CACL;MAEL,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,GAAD;KAAe,WAAU;eAAzB,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,yBAAyB,SAAS;MACpC,CAAA;KACD,CAAA,GACL,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAe,EAAK,OAAO,EAAK,QAAQ;OACrC,CAAA,GACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAA8C,MACzC,EAAmB,EAAK,QAAQ,CAC/B;SACH;UACL,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG,EAAG,wBAAwB,QAAQ;QAAG;QACtC,EAAK,aAAa,YACf,EAAG,4BAA4B,aAAa,IAC5C,EAAG,0BAA0B,UAAU;OAC1C;QACA;OACQ;QAGf,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACG,EAAG,mCAAmC,mBAAmB;QAAE;QAC3D,EAAe;QAAO;OACrB;;KACD,CAAA,GACL,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAe,WAAW,IACzB,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,4BAA4B,uCAAuC;MACtE,CAAA,IAEH,kBAAC,GAAD;OAAY,MAAM,EAAe,SAAS;OAAG,iBAAiB;iBAC5D,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAe,KAAK,MACnB,kBAAC,MAAD;SAEE,WAAU;mBAFZ,CAIE,kBAAC,OAAD;UAAK,WAAU;oBACb,kBAAC,OAAD;WACE,WAAU;WACV,MAAK;WACL,SAAQ;WACR,QAAO;qBAEP,kBAAC,QAAD;YACE,eAAc;YACd,gBAAe;YACf,aAAa;YACb,GAAE;WACH,CAAA;UACE,CAAA;SACF,CAAA,GACL,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,KAAD;YAAG,WAAU;sBAAiC,EAAM;WAAQ,CAAA,GAC5D,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAiB,EAAM,YAAY,EAAM,IAAI;WAC1C,CAAA,CACH;cACL,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CAAyD,UAChD,EAAM,SACT;eACL,EAAM,aACL,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAM;WACH,CAAA,CAEL;YACF;WACH;WApCG,EAAM,OAoCT,CACL;OACC,CAAA;MACM,CAAA;KAEX,CAAA,CACF;MACF;OAGL,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,gCAAgC,WAAW;MAC7C,CAAA;KACD,CAAA,GACL,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAA+B,EAAG,uBAAuB,OAAO;OAAK,CAAA,GAClF,kBAAC,KAAD;QAAG,WAAU;kBAAb;SACG,EAAe,EAAK,OAAO,EAAK,QAAQ;SAAE;SAAI,EAAmB,EAAK,QAAQ;QAC9E;SACA,EAAA,CAAA;OACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAG,+BAA+B,gBAAgB;OAClD,CAAA,GACH,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAwB,EAAK,QAAQ;OACrC,CAAA,CACA,EAAA,CAAA;OACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAG,gCAAgC,iBAAiB;OACpD,CAAA,GACH,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAe;OACf,CAAA,CACA,EAAA,CAAA;OACL,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAW,aAAa,EAAK,IAAI;QAChD,WAAU;kBACX;OAEO,CAAA;MACL;OACF;QAGL,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,QAAQ;KAClC,WAAU;eACX;IAEO,CAAA,CACL;KACF;IACF;MAnNH,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,8BAA8B,gBAAgB;IAChD,CAAA;IACJ,kBAAC,KAAD;KAAG,WAAU;eACV,GAAO,WACN,EACE,qCACA,8CACF;IACD,CAAA;IACH,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,QAAQ;KAClC,WAAU;eACX;IAEO,CAAA;GACL;;CACF,CAAA;AAiMX"}
|
|
1
|
+
{"version":3,"file":"PlanBrowseDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanBrowseDetailPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Browse Detail Page (User-facing)\n * View plan details and subscribe. No edit/toggle actions.\n */\n\nimport { type FC, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n formatQuotaValue,\n getPlanFeatureQuantity,\n getPlanFeatureUnit,\n getQuotaDisplayName,\n isServerError,\n} from '../../../shared/utils';\nimport type { PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ShowMore, PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport type { ReactNode } from 'react';\n\n/** Wraps children in a ShowMore progressive-disclosure region only when `when` is true. */\nconst ShowMoreIf: FC<{ when: boolean; collapsedHeight?: number; children: ReactNode }> = ({\n when,\n collapsedHeight,\n children,\n}) => (when ? <ShowMore collapsedHeight={collapsedHeight}>{children}</ShowMore> : <>{children}</>);\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n quotaType: string;\n limitType: string;\n totalValue: number;\n unit?: string;\n /** True for WORKSPACE-scoped quotas — the full amount applies to EACH\n * activated workspace, not divided across them (see plan.maxWorkspaces). */\n perWorkspace: boolean;\n}\n\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limits = feature.quota.limits as { type?: string; value?: number } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: getQuotaDisplayName(feature),\n quotaType: feature.quota.quotaType ?? '',\n limitType,\n totalValue: limitValue,\n unit: getPlanFeatureUnit(feature),\n perWorkspace: feature.quota.scope === 'WORKSPACE',\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const PlanBrowseDetailPage: 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 { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const { plan, isLoading, error, refetch } = usePlan(planId);\n\n const combinedQuotas = useMemo(() => combineQuotas(plan?.features || []), [plan?.features]);\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-10 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.plans.unableToLoadPlanDetails', 'Unable to load plan details.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!plan) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.planNotFound', 'Plan not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {error?.message ||\n tr(\n 'billing.plans.planNotFoundMessage',\n 'The plan you are looking for does not exist.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <svg className=\"size-5\" 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 <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan.name}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {formatPlanDurationLabel(plan.duration)} billing plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.plans.browseDetailPurpose',\n 'Everything this plan includes — its price, billing period and the full list of features and quotas — so you can confirm it fits before subscribing. Hit Subscribe when you are ready to check out.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Pricing Card — primary emphasis zone */}\n <EmphasisPanel className=\"overflow-hidden p-0\">\n <div className=\"p-6 border-b border-border-seam\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.pricing', 'Pricing')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </span>\n <span className=\"text-lg text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {tr('billing.plans.billed', 'Billed')}{' '}\n {plan.duration === 'monthly'\n ? tr('billing.plans.everyMonth', 'every month')\n : tr('billing.plans.annually', 'annually')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Features Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.featuresAndQuotas', 'Features & Quotas')} (\n {combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-6\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.noFeatures', 'No features configured for this plan.')}\n </p>\n ) : (\n <ShowMoreIf when={combinedQuotas.length > 4} collapsedHeight={280}>\n <ul className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <li\n key={quota.quotaId}\n className=\"flex items-start gap-3 p-3 rounded-lg bg-bg-sunken\"\n >\n <div className=\"size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0\">\n <svg\n className=\"size-4 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <span className=\"font-semibold text-text-primary\">\n {formatQuotaValue(quota.totalValue, quota.unit)}\n {quota.perWorkspace && (\n <span className=\"font-normal text-text-secondary\">\n {' '}\n {tr('billing.plans.perWorkspace', 'per workspace')}\n </span>\n )}\n </span>\n </div>\n <div className=\"flex items-center gap-2 mt-1\">\n <span className=\"text-sm text-text-secondary capitalize\">\n Type: {quota.limitType}\n </span>\n {quota.quotaType && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-secondary rounded\">\n {quota.quotaType}\n </span>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n </ShowMoreIf>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6 lg:sticky lg:top-6 lg:self-start\">\n {/* Subscribe Card — primary emphasis zone */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)] overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.subscribeTitle', 'Subscribe')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">{tr('billing.plans.price', 'Price')}</p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {formatCurrency(plan.price, plan.currency)} / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.billingPeriod', 'Billing Period')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatPlanDurationLabel(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.maxWorkspaces', 'Max Workspaces')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {plan.maxWorkspaces ?? 1}{' '}\n {(plan.maxWorkspaces ?? 1) === 1\n ? tr('billing.plans.workspaceSingular', 'workspace')\n : tr('billing.plans.workspacePlural', 'workspaces')}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.quotasIncluded', 'Quotas Included')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {combinedQuotas.length}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"w-full px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n </div>\n\n {/* Back */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -112,7 +112,7 @@ var O = (e) => typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "number" ?
|
|
|
112
112
|
return H.find((t) => t.id === e) || null;
|
|
113
113
|
}, [H, P?.planId]), W = u(() => {
|
|
114
114
|
let e = P?.planId, t = P?.plan?.productID;
|
|
115
|
-
return H.filter((n) => n.id === e || !n.isActive ? !1 : t ? n.productID === t : !0).sort((e, t) => e.price - t.price);
|
|
115
|
+
return H.filter((n) => n.id === e || !n.isActive || n.isPurchasable === !1 ? !1 : t ? n.productID === t : !0).sort((e, t) => e.price - t.price);
|
|
116
116
|
}, [H, P]), G = u(() => W.find((e) => e.id === R) || null, [W, R]), K = G !== null && G.price < (P?.plan?.price ?? 0), q = (e) => {
|
|
117
117
|
V((t) => ({
|
|
118
118
|
...t,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpgradeSubscriptionPage.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.tsx"],"sourcesContent":["/**\n * Change Plan Page\n * Allows users to change their current subscription to a different plan\n * Shows plan features in collapsible sections for easy comparison\n */\n\nimport { type FC, useState, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport {\n ArrowLeft,\n ArrowRight,\n Loader2,\n Package,\n CheckCircle,\n AlertCircle,\n ChevronDown,\n ChevronUp,\n Sparkles,\n Infinity as InfinityIcon,\n} from 'lucide-react';\n\nimport { useSubscription } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { formatCurrency, formatPlanDuration } from '../../../shared/utils/format';\nimport {\n useGetAllPlansQuery,\n useGetDefaultBillingAccountDashboardQuery,\n} from '../../../../generated/global-operations';\nimport type { Plan, PlanDuration, SubscriptionStatus } from '../../../shared/types';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\n\n// Helper type for plan features\ninterface PlanFeature {\n id: string;\n quotaId?: string;\n quota?: {\n id: string;\n name: string;\n limits?: Record<string, number | string | boolean>;\n reusable?: boolean;\n quotaType?: string;\n };\n}\n\n// Helper function to format limit values nicely\nconst formatLimitValue = (value: number | string | boolean): string => {\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (typeof value === 'number') {\n if (value === -1 || value === Infinity) return 'Unlimited';\n if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;\n if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;\n return value.toLocaleString();\n }\n return String(value);\n};\n\n// Helper function to get a user-friendly limit key name\nconst formatLimitKey = (key: string): string => {\n return key\n .replace(/_/g, ' ')\n .replace(/([A-Z])/g, ' $1')\n .replace(/^./, (str) => str.toUpperCase())\n .trim();\n};\n\n// Helper function to aggregate features by quotaId\n// Combines limits for features with the same quota\nconst aggregateFeaturesByQuota = (features: PlanFeature[]): PlanFeature[] => {\n const quotaMap = new Map<string, PlanFeature>();\n\n for (const feature of features) {\n const quotaId = feature.quotaId || feature.quota?.id;\n if (!quotaId || !feature.quota) continue;\n\n const existing = quotaMap.get(quotaId);\n if (existing && existing.quota) {\n // Aggregate limits - sum numeric values\n const existingLimits = existing.quota.limits || {};\n const newLimits = feature.quota.limits || {};\n const combinedLimits: Record<string, number | string | boolean> = { ...existingLimits };\n\n for (const [key, value] of Object.entries(newLimits)) {\n if (typeof value === 'number' && typeof combinedLimits[key] === 'number') {\n // Sum numeric values (but keep -1/unlimited as unlimited)\n const existingVal = combinedLimits[key] as number;\n if (existingVal === -1 || value === -1) {\n combinedLimits[key] = -1; // Unlimited stays unlimited\n } else {\n combinedLimits[key] = existingVal + value;\n }\n } else if (!(key in combinedLimits)) {\n combinedLimits[key] = value;\n }\n }\n\n existing.quota = {\n ...existing.quota,\n limits: combinedLimits,\n };\n } else {\n // Clone the feature to avoid mutating original\n quotaMap.set(quotaId, {\n ...feature,\n quota: feature.quota\n ? {\n ...feature.quota,\n limits: feature.quota.limits ? { ...feature.quota.limits } : undefined,\n }\n : undefined,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n};\n\n// Collapsible Features Component\ninterface CollapsibleFeaturesProps {\n features: PlanFeature[];\n isExpanded: boolean;\n onToggle: () => void;\n variant?: 'current' | 'available';\n}\n\nconst CollapsibleFeatures: FC<CollapsibleFeaturesProps> = ({\n features,\n isExpanded,\n onToggle,\n variant = 'available',\n}) => {\n // Aggregate features with same quotaId\n const aggregatedFeatures = useMemo(() => aggregateFeaturesByQuota(features), [features]);\n const featureCount = aggregatedFeatures.length;\n\n if (featureCount === 0) {\n return <p className=\"text-xs text-text-secondary italic\">No features defined</p>;\n }\n\n return (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggle();\n }}\n className={`flex items-center gap-1.5 text-xs font-medium transition-colors ${\n variant === 'current'\n ? 'text-text-secondary hover:text-text-primary'\n : 'text-text-link hover:text-text-link'\n }`}\n >\n <Sparkles className=\"size-3\" />\n <span>\n {featureCount} feature{featureCount !== 1 ? 's' : ''}\n </span>\n {isExpanded ? <ChevronUp className=\"size-3\" /> : <ChevronDown className=\"size-3\" />}\n </button>\n\n {isExpanded && (\n <div className={`mt-2 space-y-2 ${variant === 'current' ? 'pl-0' : 'pl-1'}`}>\n {aggregatedFeatures.map((feature) => {\n const quota = feature.quota;\n if (!quota) return null;\n\n const limits = quota.limits || {};\n const limitEntries = Object.entries(limits);\n\n return (\n <div\n key={feature.id}\n className={`rounded-button p-2 ${\n variant === 'current' ? 'bg-bg-sunken' : 'bg-[var(--color-accent-soft)]'\n }`}\n >\n <p\n className={`text-xs font-medium ${\n variant === 'current' ? 'text-text-primary' : 'text-text-primary'\n }`}\n >\n {quota.name}\n </p>\n {limitEntries.length > 0 && (\n <div className=\"mt-1 flex flex-wrap gap-x-3 gap-y-1\">\n {limitEntries.map(([key, value]) => (\n <div\n key={key}\n className=\"flex items-center gap-1 text-xs text-text-secondary\"\n >\n <span className=\"opacity-70\">{formatLimitKey(key)}:</span>\n <span className=\"font-medium text-text-primary flex items-center gap-0.5\">\n {value === -1 || value === Infinity ? (\n <>\n <InfinityIcon className=\"size-3\" />\n <span className=\"sr-only\">Unlimited</span>\n </>\n ) : (\n formatLimitValue(value as number | string | boolean)\n )}\n </span>\n </div>\n ))}\n </div>\n )}\n {quota.reusable && (\n <span className=\"inline-block mt-1 text-[10px] px-1.5 py-0.5 rounded bg-status-success-bg-subtle text-status-success-text\">\n Reusable\n </span>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n\nexport const UpgradeSubscriptionPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { subscriptionId } = useParams<{ subscriptionId: string }>();\n const navigateTo = useBillingNavigate();\n const { orgId } = useBilling();\n const permissions = useBillingPermissions();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n\n const { data: defaultBillingAccountData } = useGetDefaultBillingAccountDashboardQuery({\n fetchPolicy: 'cache-first',\n });\n const billingAccountId =\n selectedAccountId ?? defaultBillingAccountData?.getDefaultBillingAccount?.id ?? undefined;\n const { subscription, isLoading: subscriptionLoading } = useSubscription(\n subscriptionId,\n billingAccountId\n );\n\n // Fetch available plans (includes features)\n const { data: plansData, loading: plansLoading } = useGetAllPlansQuery();\n\n // State\n const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);\n const [expandedFeatures, setExpandedFeatures] = useState<Record<string, boolean>>({});\n\n // Get all plans including current for feature lookup\n const allPlans = useMemo(() => {\n return (plansData?.getAllPlans || []) as Plan[];\n }, [plansData]);\n\n // Get current plan with features from allPlans\n const currentPlanWithFeatures = useMemo(() => {\n const currentPlanId = subscription?.planId;\n return allPlans.find((p) => p.id === currentPlanId) || null;\n }, [allPlans, subscription?.planId]);\n\n // Filter plans - show all other active plans (same product if productID exists, otherwise all)\n const availablePlans = useMemo(() => {\n const currentPlanId = subscription?.planId;\n const currentProductId = subscription?.plan?.productID;\n\n return allPlans\n .filter((plan) => {\n // Always exclude current plan\n if (plan.id === currentPlanId) return false;\n // Only show active plans\n if (!plan.isActive) return false;\n // If we have productID, filter by same product\n if (currentProductId) {\n return plan.productID === currentProductId;\n }\n // If no productID, show all active plans\n return true;\n })\n .sort((a, b) => a.price - b.price);\n }, [allPlans, subscription]);\n\n const selectedPlan = useMemo(() => {\n return availablePlans.find((p) => p.id === selectedPlanId) || null;\n }, [availablePlans, selectedPlanId]);\n\n const isDowngrade =\n selectedPlan !== null && selectedPlan.price < (subscription?.plan?.price ?? 0);\n\n // Toggle feature expansion\n const toggleFeatures = (planId: string) => {\n setExpandedFeatures((prev) => ({\n ...prev,\n [planId]: !prev[planId],\n }));\n };\n\n // Handle proceed to checkout\n const handleProceedToCheckout = () => {\n if (!subscription || !selectedPlan) return;\n const currentPrice = subscription.plan?.price ?? 0;\n const isDowngrade = selectedPlan.price < currentPrice;\n navigateTo(`/checkout/${selectedPlan.id}`, {\n state: {\n purchaseType: 'PLAN',\n // Upgrade: old quotas transfer to new plan immediately\n // Downgrade: old plan runs to natural expiry, auto-renewal cancelled\n ...(isDowngrade\n ? { downgradeFromSubscriptionId: subscription.id }\n : { upgradeFromSubscriptionId: subscription.id }),\n currentPlanName: subscription.plan?.name,\n billingAccountId: subscription.billingAccountId,\n },\n });\n };\n\n // Permission check\n if (!permissions.canUpdateSubscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-bg-sunken flex items-center justify-center\">\n <AlertCircle className=\"size-6 text-text-secondary\" />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">\n You don't have permission to change subscriptions.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (subscriptionLoading || plansLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <Loader2 className=\"size-8 animate-spin mx-auto text-text-link\" />\n <p className=\"text-text-secondary\">Loading plans…</p>\n </div>\n </div>\n );\n }\n\n // Subscription not found\n if (!subscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <AlertCircle className=\"size-12 mx-auto text-status-error-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">Subscription not found</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button\"\n >\n {tr('billing.subscriptions.backToSubscriptions', 'Back to Subscriptions')}\n </button>\n </div>\n </div>\n );\n }\n\n // Subscription is not active (already upgraded/canceled/etc)\n const isActive = subscription.status === ('active' as SubscriptionStatus);\n if (!isActive) {\n return (\n <div className=\"max-w-2xl mx-auto p-6 space-y-6\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <div className=\"text-center space-y-4 py-12\">\n <AlertCircle className=\"size-12 mx-auto text-status-warning-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.cannotChangePlan', 'Cannot Change Plan')}\n </h2>\n <p className=\"text-sm text-text-secondary max-w-md mx-auto\">\n {tr(\n 'billing.subscriptions.notActiveMessage',\n 'This subscription is no longer active. You can only change plans for active subscriptions.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button\"\n >\n {tr('billing.subscriptions.viewAll', 'View All Subscriptions')}\n </button>\n </div>\n </div>\n );\n }\n\n // No other plans available\n if (availablePlans.length === 0) {\n return (\n <div className=\"max-w-2xl mx-auto p-6 space-y-6\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <div className=\"text-center space-y-4 py-12\">\n <Package className=\"size-12 mx-auto text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.noPlansAvailable', 'No Other Plans Available')}\n </h2>\n <p className=\"text-sm text-text-secondary max-w-md mx-auto\">\n {tr(\n 'billing.subscriptions.noPlansMessage',\n 'There are no other active plans available for this product.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const currentPlanFeatures = (currentPlanWithFeatures?.features || []) as PlanFeature[];\n\n return (\n <div className=\"max-w-3xl mx-auto p-6 space-y-6\">\n {/* Header */}\n <div>\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary mb-4\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.subscriptions.changeYourPlan', 'Change Your Plan')}\n </h1>\n <p className=\"text-text-secondary mt-1\">\n Switch from <span className=\"font-medium\">{subscription.plan?.name}</span> to a different\n plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.subscriptions.upgradePurpose',\n 'Move this subscription to a different plan — upgrade for more capacity or downgrade to save. Compare what each plan includes and see how the change affects your billing before you confirm.'\n )}\n </PagePurpose>\n </div>\n\n {/* Current Plan Card */}\n <div className=\"bg-bg-sunken border border-border-subtle rounded-lg p-4\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex-1\">\n <p className=\"text-sm text-text-secondary\">Current Plan</p>\n <p className=\"font-semibold text-text-primary\">{subscription.plan?.name}</p>\n\n {/* Current Plan Features - Collapsible */}\n <CollapsibleFeatures\n features={currentPlanFeatures}\n isExpanded={expandedFeatures['current'] || false}\n onToggle={() => toggleFeatures('current')}\n variant=\"current\"\n />\n </div>\n <div className=\"text-right shrink-0 ml-4\">\n <p className=\"text-sm text-text-secondary\">Price</p>\n <p className=\"font-semibold text-text-primary\">\n {formatCurrency(subscription.plan?.price || 0, subscription.plan?.currency || 'USD')}\n <span className=\"text-sm font-normal text-text-secondary\">\n {' '}\n / {formatPlanDuration((subscription.plan?.duration || 'monthly') as PlanDuration)}\n </span>\n </p>\n </div>\n </div>\n </div>\n\n {/* Available Plans */}\n <div className=\"space-y-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Select New Plan</h2>\n <div className=\"grid gap-3\">\n {availablePlans.map((plan) => {\n const priceDiff = plan.price - (subscription.plan?.price || 0);\n const isSelected = selectedPlanId === plan.id;\n const planFeatures = (plan.features || []) as PlanFeature[];\n const isFeaturesExpanded = expandedFeatures[plan.id] || false;\n\n return (\n <div\n key={plan.id}\n className={`rounded-lg border transition-all ${\n isSelected\n ? 'border-border-strong bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-focus-ring)]'\n : 'border-border-subtle bg-bg-surface hover:border-border-strong'\n }`}\n >\n {/* Plan Header - Clickable for selection */}\n <button\n type=\"button\"\n onClick={() => setSelectedPlanId(plan.id)}\n className=\"w-full p-4 text-left\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-3\">\n <div\n className={`size-5 mt-0.5 rounded-full border-2 flex items-center justify-center shrink-0 ${\n isSelected\n ? 'border-border-strong bg-action-primary-bg'\n : 'border-border-default'\n }`}\n >\n {isSelected && <CheckCircle className=\"size-3 text-action-primary-text\" />}\n </div>\n <div className=\"flex-1\">\n <p className=\"font-semibold text-text-primary\">{plan.name}</p>\n {priceDiff !== 0 && (\n <p\n className={`text-sm ${priceDiff > 0 ? 'text-status-warning-text' : 'text-status-success-text'}`}\n >\n {priceDiff > 0 ? '+' : ''}\n {formatCurrency(priceDiff, plan.currency)} /{' '}\n {formatPlanDuration(plan.duration)}\n </p>\n )}\n </div>\n </div>\n <div className=\"text-right shrink-0 ml-4\">\n <p className=\"font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </p>\n <p className=\"text-sm text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n </div>\n </button>\n\n {/* Features Section - Below the main content */}\n <div className=\"px-4 pb-4 pt-0 ml-8\">\n <CollapsibleFeatures\n features={planFeatures}\n isExpanded={isFeaturesExpanded}\n onToggle={() => toggleFeatures(plan.id)}\n variant=\"available\"\n />\n </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Summary & Action */}\n {selectedPlan &&\n (() => {\n const currentAggregatedCount = aggregateFeaturesByQuota(currentPlanFeatures).length;\n const selectedPlanFeatures = (selectedPlan.features || []) as PlanFeature[];\n const selectedAggregatedCount = aggregateFeaturesByQuota(selectedPlanFeatures).length;\n\n return (\n <div className=\"bg-bg-surface border border-border-subtle rounded-lg p-4 space-y-4\">\n {/* Plan change visualization */}\n <div className=\"flex items-center justify-center gap-4 py-2\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">From</p>\n <p className=\"font-medium text-text-primary\">{subscription.plan?.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {currentAggregatedCount} feature{currentAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n <ArrowRight className=\"size-5 text-text-secondary\" />\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">To</p>\n <p className=\"font-medium text-text-link\">{selectedPlan.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {selectedAggregatedCount} feature{selectedAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n\n {/* Info message */}\n <p className=\"text-sm text-text-secondary text-center\">\n You'll be taken to checkout to select payment method and complete the change.\n </p>\n\n {/* Action Buttons */}\n <div className=\"flex items-center justify-end gap-3 pt-2 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(\n withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId)\n )\n }\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleProceedToCheckout}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n {isDowngrade ? 'Downgrade Plan' : 'Upgrade Plan'}\n </button>\n </div>\n </div>\n );\n })()}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UpgradeSubscriptionPage.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.tsx"],"sourcesContent":["/**\n * Change Plan Page\n * Allows users to change their current subscription to a different plan\n * Shows plan features in collapsible sections for easy comparison\n */\n\nimport { type FC, useState, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport {\n ArrowLeft,\n ArrowRight,\n Loader2,\n Package,\n CheckCircle,\n AlertCircle,\n ChevronDown,\n ChevronUp,\n Sparkles,\n Infinity as InfinityIcon,\n} from 'lucide-react';\n\nimport { useSubscription } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { formatCurrency, formatPlanDuration } from '../../../shared/utils/format';\nimport {\n useGetAllPlansQuery,\n useGetDefaultBillingAccountDashboardQuery,\n} from '../../../../generated/global-operations';\nimport type { Plan, PlanDuration, SubscriptionStatus } from '../../../shared/types';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\n\n// Helper type for plan features\ninterface PlanFeature {\n id: string;\n quotaId?: string;\n quota?: {\n id: string;\n name: string;\n limits?: Record<string, number | string | boolean>;\n reusable?: boolean;\n quotaType?: string;\n };\n}\n\n// Helper function to format limit values nicely\nconst formatLimitValue = (value: number | string | boolean): string => {\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (typeof value === 'number') {\n if (value === -1 || value === Infinity) return 'Unlimited';\n if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;\n if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;\n return value.toLocaleString();\n }\n return String(value);\n};\n\n// Helper function to get a user-friendly limit key name\nconst formatLimitKey = (key: string): string => {\n return key\n .replace(/_/g, ' ')\n .replace(/([A-Z])/g, ' $1')\n .replace(/^./, (str) => str.toUpperCase())\n .trim();\n};\n\n// Helper function to aggregate features by quotaId\n// Combines limits for features with the same quota\nconst aggregateFeaturesByQuota = (features: PlanFeature[]): PlanFeature[] => {\n const quotaMap = new Map<string, PlanFeature>();\n\n for (const feature of features) {\n const quotaId = feature.quotaId || feature.quota?.id;\n if (!quotaId || !feature.quota) continue;\n\n const existing = quotaMap.get(quotaId);\n if (existing && existing.quota) {\n // Aggregate limits - sum numeric values\n const existingLimits = existing.quota.limits || {};\n const newLimits = feature.quota.limits || {};\n const combinedLimits: Record<string, number | string | boolean> = { ...existingLimits };\n\n for (const [key, value] of Object.entries(newLimits)) {\n if (typeof value === 'number' && typeof combinedLimits[key] === 'number') {\n // Sum numeric values (but keep -1/unlimited as unlimited)\n const existingVal = combinedLimits[key] as number;\n if (existingVal === -1 || value === -1) {\n combinedLimits[key] = -1; // Unlimited stays unlimited\n } else {\n combinedLimits[key] = existingVal + value;\n }\n } else if (!(key in combinedLimits)) {\n combinedLimits[key] = value;\n }\n }\n\n existing.quota = {\n ...existing.quota,\n limits: combinedLimits,\n };\n } else {\n // Clone the feature to avoid mutating original\n quotaMap.set(quotaId, {\n ...feature,\n quota: feature.quota\n ? {\n ...feature.quota,\n limits: feature.quota.limits ? { ...feature.quota.limits } : undefined,\n }\n : undefined,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n};\n\n// Collapsible Features Component\ninterface CollapsibleFeaturesProps {\n features: PlanFeature[];\n isExpanded: boolean;\n onToggle: () => void;\n variant?: 'current' | 'available';\n}\n\nconst CollapsibleFeatures: FC<CollapsibleFeaturesProps> = ({\n features,\n isExpanded,\n onToggle,\n variant = 'available',\n}) => {\n // Aggregate features with same quotaId\n const aggregatedFeatures = useMemo(() => aggregateFeaturesByQuota(features), [features]);\n const featureCount = aggregatedFeatures.length;\n\n if (featureCount === 0) {\n return <p className=\"text-xs text-text-secondary italic\">No features defined</p>;\n }\n\n return (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggle();\n }}\n className={`flex items-center gap-1.5 text-xs font-medium transition-colors ${\n variant === 'current'\n ? 'text-text-secondary hover:text-text-primary'\n : 'text-text-link hover:text-text-link'\n }`}\n >\n <Sparkles className=\"size-3\" />\n <span>\n {featureCount} feature{featureCount !== 1 ? 's' : ''}\n </span>\n {isExpanded ? <ChevronUp className=\"size-3\" /> : <ChevronDown className=\"size-3\" />}\n </button>\n\n {isExpanded && (\n <div className={`mt-2 space-y-2 ${variant === 'current' ? 'pl-0' : 'pl-1'}`}>\n {aggregatedFeatures.map((feature) => {\n const quota = feature.quota;\n if (!quota) return null;\n\n const limits = quota.limits || {};\n const limitEntries = Object.entries(limits);\n\n return (\n <div\n key={feature.id}\n className={`rounded-button p-2 ${\n variant === 'current' ? 'bg-bg-sunken' : 'bg-[var(--color-accent-soft)]'\n }`}\n >\n <p\n className={`text-xs font-medium ${\n variant === 'current' ? 'text-text-primary' : 'text-text-primary'\n }`}\n >\n {quota.name}\n </p>\n {limitEntries.length > 0 && (\n <div className=\"mt-1 flex flex-wrap gap-x-3 gap-y-1\">\n {limitEntries.map(([key, value]) => (\n <div\n key={key}\n className=\"flex items-center gap-1 text-xs text-text-secondary\"\n >\n <span className=\"opacity-70\">{formatLimitKey(key)}:</span>\n <span className=\"font-medium text-text-primary flex items-center gap-0.5\">\n {value === -1 || value === Infinity ? (\n <>\n <InfinityIcon className=\"size-3\" />\n <span className=\"sr-only\">Unlimited</span>\n </>\n ) : (\n formatLimitValue(value as number | string | boolean)\n )}\n </span>\n </div>\n ))}\n </div>\n )}\n {quota.reusable && (\n <span className=\"inline-block mt-1 text-[10px] px-1.5 py-0.5 rounded bg-status-success-bg-subtle text-status-success-text\">\n Reusable\n </span>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n\nexport const UpgradeSubscriptionPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { subscriptionId } = useParams<{ subscriptionId: string }>();\n const navigateTo = useBillingNavigate();\n const { orgId } = useBilling();\n const permissions = useBillingPermissions();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n\n const { data: defaultBillingAccountData } = useGetDefaultBillingAccountDashboardQuery({\n fetchPolicy: 'cache-first',\n });\n const billingAccountId =\n selectedAccountId ?? defaultBillingAccountData?.getDefaultBillingAccount?.id ?? undefined;\n const { subscription, isLoading: subscriptionLoading } = useSubscription(\n subscriptionId,\n billingAccountId\n );\n\n // Fetch available plans (includes features)\n const { data: plansData, loading: plansLoading } = useGetAllPlansQuery();\n\n // State\n const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);\n const [expandedFeatures, setExpandedFeatures] = useState<Record<string, boolean>>({});\n\n // Get all plans including current for feature lookup\n const allPlans = useMemo(() => {\n return (plansData?.getAllPlans || []) as Plan[];\n }, [plansData]);\n\n // Get current plan with features from allPlans\n const currentPlanWithFeatures = useMemo(() => {\n const currentPlanId = subscription?.planId;\n return allPlans.find((p) => p.id === currentPlanId) || null;\n }, [allPlans, subscription?.planId]);\n\n // Filter plans - show all other active plans (same product if productID exists, otherwise all)\n const availablePlans = useMemo(() => {\n const currentPlanId = subscription?.planId;\n const currentProductId = subscription?.plan?.productID;\n\n return allPlans\n .filter((plan) => {\n // Always exclude current plan\n if (plan.id === currentPlanId) return false;\n // Only show active plans\n if (!plan.isActive) return false;\n // Exclude non-self-serve plans (Enterprise \"Contact Sales\", Internal\n // team plans, etc.) — getAllPlans doesn't filter these out itself\n // (unlike publicPlans), and this is a self-serve upgrade flow: a plan\n // you can't purchase directly has no business being a selectable\n // option here.\n if (plan.isPurchasable === false) return false;\n // If we have productID, filter by same product\n if (currentProductId) {\n return plan.productID === currentProductId;\n }\n // If no productID, show all active plans\n return true;\n })\n .sort((a, b) => a.price - b.price);\n }, [allPlans, subscription]);\n\n const selectedPlan = useMemo(() => {\n return availablePlans.find((p) => p.id === selectedPlanId) || null;\n }, [availablePlans, selectedPlanId]);\n\n const isDowngrade =\n selectedPlan !== null && selectedPlan.price < (subscription?.plan?.price ?? 0);\n\n // Toggle feature expansion\n const toggleFeatures = (planId: string) => {\n setExpandedFeatures((prev) => ({\n ...prev,\n [planId]: !prev[planId],\n }));\n };\n\n // Handle proceed to checkout\n const handleProceedToCheckout = () => {\n if (!subscription || !selectedPlan) return;\n const currentPrice = subscription.plan?.price ?? 0;\n const isDowngrade = selectedPlan.price < currentPrice;\n navigateTo(`/checkout/${selectedPlan.id}`, {\n state: {\n purchaseType: 'PLAN',\n // Upgrade: old quotas transfer to new plan immediately\n // Downgrade: old plan runs to natural expiry, auto-renewal cancelled\n ...(isDowngrade\n ? { downgradeFromSubscriptionId: subscription.id }\n : { upgradeFromSubscriptionId: subscription.id }),\n currentPlanName: subscription.plan?.name,\n billingAccountId: subscription.billingAccountId,\n },\n });\n };\n\n // Permission check\n if (!permissions.canUpdateSubscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-bg-sunken flex items-center justify-center\">\n <AlertCircle className=\"size-6 text-text-secondary\" />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">\n You don't have permission to change subscriptions.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (subscriptionLoading || plansLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <Loader2 className=\"size-8 animate-spin mx-auto text-text-link\" />\n <p className=\"text-text-secondary\">Loading plans…</p>\n </div>\n </div>\n );\n }\n\n // Subscription not found\n if (!subscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <AlertCircle className=\"size-12 mx-auto text-status-error-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">Subscription not found</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button\"\n >\n {tr('billing.subscriptions.backToSubscriptions', 'Back to Subscriptions')}\n </button>\n </div>\n </div>\n );\n }\n\n // Subscription is not active (already upgraded/canceled/etc)\n const isActive = subscription.status === ('active' as SubscriptionStatus);\n if (!isActive) {\n return (\n <div className=\"max-w-2xl mx-auto p-6 space-y-6\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <div className=\"text-center space-y-4 py-12\">\n <AlertCircle className=\"size-12 mx-auto text-status-warning-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.cannotChangePlan', 'Cannot Change Plan')}\n </h2>\n <p className=\"text-sm text-text-secondary max-w-md mx-auto\">\n {tr(\n 'billing.subscriptions.notActiveMessage',\n 'This subscription is no longer active. You can only change plans for active subscriptions.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button\"\n >\n {tr('billing.subscriptions.viewAll', 'View All Subscriptions')}\n </button>\n </div>\n </div>\n );\n }\n\n // No other plans available\n if (availablePlans.length === 0) {\n return (\n <div className=\"max-w-2xl mx-auto p-6 space-y-6\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <div className=\"text-center space-y-4 py-12\">\n <Package className=\"size-12 mx-auto text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.noPlansAvailable', 'No Other Plans Available')}\n </h2>\n <p className=\"text-sm text-text-secondary max-w-md mx-auto\">\n {tr(\n 'billing.subscriptions.noPlansMessage',\n 'There are no other active plans available for this product.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const currentPlanFeatures = (currentPlanWithFeatures?.features || []) as PlanFeature[];\n\n return (\n <div className=\"max-w-3xl mx-auto p-6 space-y-6\">\n {/* Header */}\n <div>\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId))\n }\n className=\"flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary mb-4\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.subscriptions.backToSubscription', 'Back to Subscription')}\n </button>\n\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.subscriptions.changeYourPlan', 'Change Your Plan')}\n </h1>\n <p className=\"text-text-secondary mt-1\">\n Switch from <span className=\"font-medium\">{subscription.plan?.name}</span> to a different\n plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.subscriptions.upgradePurpose',\n 'Move this subscription to a different plan — upgrade for more capacity or downgrade to save. Compare what each plan includes and see how the change affects your billing before you confirm.'\n )}\n </PagePurpose>\n </div>\n\n {/* Current Plan Card */}\n <div className=\"bg-bg-sunken border border-border-subtle rounded-lg p-4\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex-1\">\n <p className=\"text-sm text-text-secondary\">Current Plan</p>\n <p className=\"font-semibold text-text-primary\">{subscription.plan?.name}</p>\n\n {/* Current Plan Features - Collapsible */}\n <CollapsibleFeatures\n features={currentPlanFeatures}\n isExpanded={expandedFeatures['current'] || false}\n onToggle={() => toggleFeatures('current')}\n variant=\"current\"\n />\n </div>\n <div className=\"text-right shrink-0 ml-4\">\n <p className=\"text-sm text-text-secondary\">Price</p>\n <p className=\"font-semibold text-text-primary\">\n {formatCurrency(subscription.plan?.price || 0, subscription.plan?.currency || 'USD')}\n <span className=\"text-sm font-normal text-text-secondary\">\n {' '}\n / {formatPlanDuration((subscription.plan?.duration || 'monthly') as PlanDuration)}\n </span>\n </p>\n </div>\n </div>\n </div>\n\n {/* Available Plans */}\n <div className=\"space-y-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Select New Plan</h2>\n <div className=\"grid gap-3\">\n {availablePlans.map((plan) => {\n const priceDiff = plan.price - (subscription.plan?.price || 0);\n const isSelected = selectedPlanId === plan.id;\n const planFeatures = (plan.features || []) as PlanFeature[];\n const isFeaturesExpanded = expandedFeatures[plan.id] || false;\n\n return (\n <div\n key={plan.id}\n className={`rounded-lg border transition-all ${\n isSelected\n ? 'border-border-strong bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-focus-ring)]'\n : 'border-border-subtle bg-bg-surface hover:border-border-strong'\n }`}\n >\n {/* Plan Header - Clickable for selection */}\n <button\n type=\"button\"\n onClick={() => setSelectedPlanId(plan.id)}\n className=\"w-full p-4 text-left\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-3\">\n <div\n className={`size-5 mt-0.5 rounded-full border-2 flex items-center justify-center shrink-0 ${\n isSelected\n ? 'border-border-strong bg-action-primary-bg'\n : 'border-border-default'\n }`}\n >\n {isSelected && <CheckCircle className=\"size-3 text-action-primary-text\" />}\n </div>\n <div className=\"flex-1\">\n <p className=\"font-semibold text-text-primary\">{plan.name}</p>\n {priceDiff !== 0 && (\n <p\n className={`text-sm ${priceDiff > 0 ? 'text-status-warning-text' : 'text-status-success-text'}`}\n >\n {priceDiff > 0 ? '+' : ''}\n {formatCurrency(priceDiff, plan.currency)} /{' '}\n {formatPlanDuration(plan.duration)}\n </p>\n )}\n </div>\n </div>\n <div className=\"text-right shrink-0 ml-4\">\n <p className=\"font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </p>\n <p className=\"text-sm text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n </div>\n </button>\n\n {/* Features Section - Below the main content */}\n <div className=\"px-4 pb-4 pt-0 ml-8\">\n <CollapsibleFeatures\n features={planFeatures}\n isExpanded={isFeaturesExpanded}\n onToggle={() => toggleFeatures(plan.id)}\n variant=\"available\"\n />\n </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Summary & Action */}\n {selectedPlan &&\n (() => {\n const currentAggregatedCount = aggregateFeaturesByQuota(currentPlanFeatures).length;\n const selectedPlanFeatures = (selectedPlan.features || []) as PlanFeature[];\n const selectedAggregatedCount = aggregateFeaturesByQuota(selectedPlanFeatures).length;\n\n return (\n <div className=\"bg-bg-surface border border-border-subtle rounded-lg p-4 space-y-4\">\n {/* Plan change visualization */}\n <div className=\"flex items-center justify-center gap-4 py-2\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">From</p>\n <p className=\"font-medium text-text-primary\">{subscription.plan?.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {currentAggregatedCount} feature{currentAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n <ArrowRight className=\"size-5 text-text-secondary\" />\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">To</p>\n <p className=\"font-medium text-text-link\">{selectedPlan.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {selectedAggregatedCount} feature{selectedAggregatedCount !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n\n {/* Info message */}\n <p className=\"text-sm text-text-secondary text-center\">\n You'll be taken to checkout to select payment method and complete the change.\n </p>\n\n {/* Action Buttons */}\n <div className=\"flex items-center justify-end gap-3 pt-2 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(\n withBillingAccountId(`/subscriptions/${subscriptionId}`, billingAccountId)\n )\n }\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleProceedToCheckout}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n {isDowngrade ? 'Downgrade Plan' : 'Upgrade Plan'}\n </button>\n </div>\n </div>\n );\n })()}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAkDA,IAAM,KAAoB,MACpB,OAAO,KAAU,YACZ,IAAQ,QAAQ,OAErB,OAAO,KAAU,WACf,MAAU,MAAM,MAAU,WAAiB,cAC3C,KAAS,MAAgB,IAAI,IAAQ,KAAS,QAAQ,CAAC,EAAE,KACzD,KAAS,MAAa,IAAI,IAAQ,KAAM,QAAQ,CAAC,EAAE,KAChD,EAAM,eAAe,IAEvB,OAAO,CAAK,GAIf,KAAkB,MACf,EACJ,QAAQ,MAAM,GAAG,EACjB,QAAQ,YAAY,KAAK,EACzB,QAAQ,OAAO,MAAQ,EAAI,YAAY,CAAC,EACxC,KAAK,GAKJ,KAA4B,MAA2C;CAC3E,IAAM,oBAAW,IAAI,IAAyB;CAE9C,KAAK,IAAM,KAAW,GAAU;EAC9B,IAAM,IAAU,EAAQ,WAAW,EAAQ,OAAO;EAClD,IAAI,CAAC,KAAW,CAAC,EAAQ,OAAO;EAEhC,IAAM,IAAW,EAAS,IAAI,CAAO;EACrC,IAAI,KAAY,EAAS,OAAO;GAE9B,IAAM,IAAiB,EAAS,MAAM,UAAU,CAAC,GAC3C,IAAY,EAAQ,MAAM,UAAU,CAAC,GACrC,IAA4D,EAAE,GAAG,EAAe;GAEtF,KAAK,IAAM,CAAC,GAAK,MAAU,OAAO,QAAQ,CAAS,GACjD,IAAI,OAAO,KAAU,YAAY,OAAO,EAAe,MAAS,UAAU;IAExE,IAAM,IAAc,EAAe;IACnC,AAAI,MAAgB,MAAM,MAAU,KAClC,EAAe,KAAO,KAEtB,EAAe,KAAO,IAAc;GAExC,OAAO,AAAM,KAAO,MAClB,EAAe,KAAO;GAI1B,EAAS,QAAQ;IACf,GAAG,EAAS;IACZ,QAAQ;GACV;EACF,OAEE,EAAS,IAAI,GAAS;GACpB,GAAG;GACH,OAAO,EAAQ,QACX;IACE,GAAG,EAAQ;IACX,QAAQ,EAAQ,MAAM,SAAS,EAAE,GAAG,EAAQ,MAAM,OAAO,IAAI,KAAA;GAC/D,IACA,KAAA;EACN,CAAC;CAEL;CAEA,OAAO,MAAM,KAAK,EAAS,OAAO,CAAC;AACrC,GAUM,KAAqD,EACzD,aACA,eACA,aACA,aAAU,kBACN;CAEJ,IAAM,IAAqB,QAAc,EAAyB,CAAQ,GAAG,CAAC,CAAQ,CAAC,GACjF,IAAe,EAAmB;CAMxC,OAJI,MAAiB,IACZ,kBAAC,KAAD;EAAG,WAAU;YAAqC;CAAsB,CAAA,IAI/E,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,MAAK;GACL,UAAU,MAAM;IAEd,AADA,EAAE,gBAAgB,GAClB,EAAS;GACX;GACA,WAAW,mEACT,MAAY,YACR,gDACA;aATR;IAYE,kBAAC,GAAD,EAAU,WAAU,SAAU,CAAA;IAC9B,kBAAC,QAAD,EAAA,UAAA;KACG;KAAa;KAAS,MAAiB,IAAU,KAAN;IACxC,EAAA,CAAA;IACQ,EAAb,IAAc,IAAmC,GAApC,EAAW,WAAU,SAAU,CAAqC;GAC5E;MAEP,KACC,kBAAC,OAAD;GAAK,WAAW,kBAAkB,MAAY,YAAY,SAAS;aAChE,EAAmB,KAAK,MAAY;IACnC,IAAM,IAAQ,EAAQ;IACtB,IAAI,CAAC,GAAO,OAAO;IAEnB,IAAM,IAAS,EAAM,UAAU,CAAC,GAC1B,IAAe,OAAO,QAAQ,CAAM;IAE1C,OACE,kBAAC,OAAD;KAEE,WAAW,sBACT,MAAY,YAAY,iBAAiB;eAH7C;MAME,kBAAC,KAAD;OACE,WAAW;iBAIV,EAAM;MACN,CAAA;MACF,EAAa,SAAS,KACrB,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAa,KAAK,CAAC,GAAK,OACvB,kBAAC,OAAD;QAEE,WAAU;kBAFZ,CAIE,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAA8B,EAAe,CAAG,GAAE,GAAO;YACzD,kBAAC,QAAD;SAAM,WAAU;mBACb,MAAU,MAAM,MAAU,WACzB,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAc,WAAU,SAAU,CAAA,GAClC,kBAAC,QAAD;UAAM,WAAU;oBAAU;SAAe,CAAA,CACzC,EAAA,CAAA,IAEF,EAAiB,CAAkC;QAEjD,CAAA,CACH;UAdE,CAcF,CACN;MACE,CAAA;MAEN,EAAM,YACL,kBAAC,QAAD;OAAM,WAAU;iBAA2G;MAErH,CAAA;KAEL;OAvCE,EAAQ,EAuCV;GAET,CAAC;EACE,CAAA,CAEJ;;AAET,GAEa,UAAoC;CAC/C,IAAM,EAAE,SAAM,EAAQ,GAChB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,CAAG;EACxB,OAAO,MAAe,IAAM,IAAW;CACzC,GACM,EAAE,sBAAmB,EAAsC,GAC3D,IAAa,EAAmB,GAChC,EAAE,aAAU,EAAW,GACvB,IAAc,EAAsB,GACpC,EAAE,yBAAsB,EAA2B;EACvD;EACA,aAAa;EACb,cAAc;CAChB,CAAC,GAEK,EAAE,MAAM,MAA8B,EAA0C,EACpF,aAAa,cACf,CAAC,GACK,IACJ,KAAqB,GAA2B,0BAA0B,MAAM,KAAA,GAC5E,EAAE,iBAAc,WAAW,MAAwB,EACvD,GACA,CACF,GAGM,EAAE,MAAM,GAAW,SAAS,MAAiB,EAAoB,GAGjE,CAAC,GAAgB,KAAqB,EAAwB,IAAI,GAClE,CAAC,GAAkB,KAAuB,EAAkC,CAAC,CAAC,GAG9E,IAAW,QACP,GAAW,eAAe,CAAC,GAClC,CAAC,CAAS,CAAC,GAGR,IAA0B,QAAc;EAC5C,IAAM,IAAgB,GAAc;EACpC,OAAO,EAAS,MAAM,MAAM,EAAE,OAAO,CAAa,KAAK;CACzD,GAAG,CAAC,GAAU,GAAc,MAAM,CAAC,GAG7B,IAAiB,QAAc;EACnC,IAAM,IAAgB,GAAc,QAC9B,IAAmB,GAAc,MAAM;EAE7C,OAAO,EACJ,QAAQ,MAEH,EAAK,OAAO,KAEZ,CAAC,EAAK,YAMN,EAAK,kBAAkB,KAAc,KAErC,IACK,EAAK,cAAc,IAGrB,EACR,EACA,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;CACrC,GAAG,CAAC,GAAU,CAAY,CAAC,GAErB,IAAe,QACZ,EAAe,MAAM,MAAM,EAAE,OAAO,CAAc,KAAK,MAC7D,CAAC,GAAgB,CAAc,CAAC,GAE7B,IACJ,MAAiB,QAAQ,EAAa,SAAS,GAAc,MAAM,SAAS,IAGxE,KAAkB,MAAmB;EACzC,GAAqB,OAAU;GAC7B,GAAG;IACF,IAAS,CAAC,EAAK;EAClB,EAAE;CACJ,GAGM,UAAgC;EACpC,IAAI,CAAC,KAAgB,CAAC,GAAc;EACpC,IAAM,IAAe,EAAa,MAAM,SAAS,GAC3C,IAAc,EAAa,QAAQ;EACzC,EAAW,aAAa,EAAa,MAAM,EACzC,OAAO;GACL,cAAc;GAGd,GAAI,IACA,EAAE,6BAA6B,EAAa,GAAG,IAC/C,EAAE,2BAA2B,EAAa,GAAG;GACjD,iBAAiB,EAAa,MAAM;GACpC,kBAAkB,EAAa;EACjC,EACF,CAAC;CACH;CAGA,IAAI,CAAC,EAAY,uBACf,OACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAa,WAAU,6BAA8B,CAAA;IAClD,CAAA;IACL,kBAAC,MAAD;KAAI,WAAU;eAA0C;IAAiB,CAAA;IACzE,kBAAC,KAAD;KAAG,WAAU;eAAuC;IAEjD,CAAA;GACA;;CACF,CAAA;CAKT,IAAI,KAAuB,GACzB,OACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAS,WAAU,6CAA8C,CAAA,GACjE,kBAAC,KAAD;IAAG,WAAU;cAAsB;GAAiB,CAAA,CACjD;;CACF,CAAA;CAKT,IAAI,CAAC,GACH,OACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD,EAAa,WAAU,yCAA0C,CAAA;IACjE,kBAAC,MAAD;KAAI,WAAU;eAA0C;IAA0B,CAAA;IAClF,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,EAAqB,kBAAkB,CAAgB,CAAC;KAClF,WAAU;eAET,EAAG,6CAA6C,uBAAuB;IAClE,CAAA;GACL;;CACF,CAAA;CAMT,IADiB,EAAa,WAAY,UAExC,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eACE,EAAW,EAAqB,kBAAkB,KAAkB,CAAgB,CAAC;GAEvF,WAAU;aALZ,CAOE,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA,GAC9B,EAAG,4CAA4C,sBAAsB,CAChE;MAER,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD,EAAa,WAAU,2CAA4C,CAAA;IACnE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,0CAA0C,oBAAoB;IAChE,CAAA;IACJ,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,0CACA,4FACF;IACC,CAAA;IACH,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,EAAqB,kBAAkB,CAAgB,CAAC;KAClF,WAAU;eAET,EAAG,iCAAiC,wBAAwB;IACvD,CAAA;GACL;IACF;;CAKT,IAAI,EAAe,WAAW,GAC5B,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eACE,EAAW,EAAqB,kBAAkB,KAAkB,CAAgB,CAAC;GAEvF,WAAU;aALZ,CAOE,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA,GAC9B,EAAG,4CAA4C,sBAAsB,CAChE;MAER,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD,EAAS,WAAU,sCAAuC,CAAA;IAC1D,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,0CAA0C,0BAA0B;IACtE,CAAA;IACJ,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,wCACA,6DACF;IACC,CAAA;GACA;IACF;;CAIT,IAAM,IAAuB,GAAyB,YAAY,CAAC;CAEnE,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,UAAD;KACE,MAAK;KACL,eACE,EAAW,EAAqB,kBAAkB,KAAkB,CAAgB,CAAC;KAEvF,WAAU;eALZ,CAOE,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA,GAC9B,EAAG,4CAA4C,sBAAsB,CAChE;;IAER,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,wCAAwC,kBAAkB;IAC5D,CAAA;IACJ,kBAAC,KAAD;KAAG,WAAU;eAAb;MAAwC;MAC1B,kBAAC,QAAD;OAAM,WAAU;iBAAe,EAAa,MAAM;MAAW,CAAA;MAAC;KAEzE;;IACH,kBAAC,GAAD;KAAa,WAAU;eACpB,EACC,wCACA,8LACF;IACW,CAAA;GACV,EAAA,CAAA;GAGL,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,KAAD;QAAG,WAAU;kBAA8B;OAAe,CAAA;OAC1D,kBAAC,KAAD;QAAG,WAAU;kBAAmC,EAAa,MAAM;OAAQ,CAAA;OAG3E,kBAAC,GAAD;QACE,UAAU;QACV,YAAY,EAAiB,WAAc;QAC3C,gBAAgB,EAAe,SAAS;QACxC,SAAQ;OACT,CAAA;MACE;SACL,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,KAAD;OAAG,WAAU;iBAA8B;MAAQ,CAAA,GACnD,kBAAC,KAAD;OAAG,WAAU;iBAAb,CACG,EAAe,EAAa,MAAM,SAAS,GAAG,EAAa,MAAM,YAAY,KAAK,GACnF,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG;SAAI;SACF,EAAoB,EAAa,MAAM,YAAY,SAA0B;QAC5E;SACL;QACA;OACF;;GACF,CAAA;GAGL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAA0C;IAAmB,CAAA,GAC3E,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAe,KAAK,MAAS;MAC5B,IAAM,IAAY,EAAK,SAAS,EAAa,MAAM,SAAS,IACtD,IAAa,MAAmB,EAAK,IACrC,IAAgB,EAAK,YAAY,CAAC,GAClC,IAAqB,EAAiB,EAAK,OAAO;MAExD,OACE,kBAAC,OAAD;OAEE,WAAW,oCACT,IACI,6FACA;iBALR,CASE,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAkB,EAAK,EAAE;QACxC,WAAU;kBAEV,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD;WACE,WAAW,iFACT,IACI,8CACA;qBAGL,KAAc,kBAAC,GAAD,EAAa,WAAU,kCAAmC,CAAA;UACtE,CAAA,GACL,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,KAAD;YAAG,WAAU;sBAAmC,EAAK;WAAQ,CAAA,GAC5D,MAAc,KACb,kBAAC,KAAD;YACE,WAAW,WAAW,IAAY,IAAI,6BAA6B;sBADrE;aAGG,IAAY,IAAI,MAAM;aACtB,EAAe,GAAW,EAAK,QAAQ;aAAE;aAAG;aAC5C,EAAmB,EAAK,QAAQ;YAChC;aAEF;YACF;aACL,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAe,EAAK,OAAO,EAAK,QAAQ;UACxC,CAAA,GACH,kBAAC,KAAD;WAAG,WAAU;qBAAb,CAA2C,MACtC,EAAmB,EAAK,QAAQ,CAClC;YACA;WACF;;OACC,CAAA,GAGR,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD;SACE,UAAU;SACV,YAAY;SACZ,gBAAgB,EAAe,EAAK,EAAE;SACtC,SAAQ;QACT,CAAA;OACE,CAAA,CACF;SAzDE,EAAK,EAyDP;KAET,CAAC;IACE,CAAA,CACF;;GAGJ,YACQ;IACL,IAAM,IAAyB,EAAyB,CAAmB,EAAE,QAEvE,IAA0B,EADF,EAAa,YAAY,CAAC,CACqB,EAAE;IAE/E,OACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MAEE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,KAAD;WAAG,WAAU;qBAA8B;UAAO,CAAA;UAClD,kBAAC,KAAD;WAAG,WAAU;qBAAiC,EAAa,MAAM;UAAQ,CAAA;UACzE,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG;YAAuB;YAAS,MAA2B,IAAU,KAAN;WAC/D;;SACA;;QACL,kBAAC,GAAD,EAAY,WAAU,6BAA8B,CAAA;QACpD,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,KAAD;WAAG,WAAU;qBAA8B;UAAK,CAAA;UAChD,kBAAC,KAAD;WAAG,WAAU;qBAA8B,EAAa;UAAQ,CAAA;UAChE,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG;YAAwB;YAAS,MAA4B,IAAU,KAAN;WACjE;;SACA;;OACF;;MAGL,kBAAC,KAAD;OAAG,WAAU;iBAA0C;MAEpD,CAAA;MAGH,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,UAAD;QACE,MAAK;QACL,eACE,EACE,EAAqB,kBAAkB,KAAkB,CAAgB,CAC3E;QAEF,WAAU;kBACX;OAEO,CAAA,GACR,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,WAAU;kBAET,IAAc,mBAAmB;OAC5B,CAAA,CACL;;KACF;;GAET,GAAG;EACF;;AAET"}
|