@burdenoff/microfe-billing 2026.625.1 → 2026.625.2
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/modules/dashboard/components/RecommendedAddonsSection.js +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/shared/ui/Card.js +1 -1
- package/dist/billing/shared/ui/Card.js.map +1 -1
- package/package.json +2 -2
|
@@ -27,7 +27,7 @@ var v = ({ addon: e, onView: r, onPurchase: a, onAddToCart: o, isInCart: d = !1,
|
|
|
27
27
|
maximumFractionDigits: 2
|
|
28
28
|
}).format(e), w = (e) => e === "yearly" ? "year" : "month", T = t(() => _(e.features || []), [e.features]);
|
|
29
29
|
return /* @__PURE__ */ h("article", {
|
|
30
|
-
className: "flex flex-col border border-border-seam rounded-card bg-bg-surface shadow-[var(--shadow-pop)] overflow-hidden
|
|
30
|
+
className: "flex flex-col border border-border-seam rounded-card bg-bg-surface shadow-[var(--shadow-pop)] overflow-hidden hover:shadow-elevation-2 [@media(hover:none)]: transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)]",
|
|
31
31
|
children: [
|
|
32
32
|
/* @__PURE__ */ m("header", {
|
|
33
33
|
className: "p-4 border-b border-border-subtle",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecommendedAddonsSection.js","names":[],"sources":["../../../../../src/billing/modules/dashboard/components/RecommendedAddonsSection.tsx"],"sourcesContent":["/**\n * Recommended Addons Section Component\n * Displays recommended addons that users might want to purchase\n * Supports add to cart functionality\n */\n\nimport { type FC, useState, useMemo } from 'react';\nimport {\n Package,\n ArrowRight,\n Sparkles,\n ChevronRight,\n Loader2,\n ShoppingCart,\n Check,\n Plus,\n Minus,\n Trash2,\n} from 'lucide-react';\nimport type { Addon, PlanDuration, SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { getPlanFeatureQuantity } from '../../../shared/utils/planFeatureQuantity';\n\n/**\n * Combined quota info for display\n */\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\n/**\n * Combines duplicate quotas by quota ID and aggregates their values\n * e.g., 10 bots + 10 bots = 20 bots (shown as one feature)\n */\nfunction combineQuotas(features: SubscriptionFeatures[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n\n for (const feature of features) {\n if (!feature.quota) continue;\n\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n}\n\ninterface RecommendedAddonsSectionProps {\n addons: Addon[];\n hasSubscription: boolean;\n onViewAddon: (addonId: string) => void;\n onPurchaseAddon: (addonId: string) => void;\n onViewAllAddons: () => void;\n /** Handler for adding addon to cart */\n onAddToCart?: (addon: Addon) => void;\n /** Check if addon is in cart */\n isInCart?: (addonId: string) => boolean;\n /** Get cart quantity for addon */\n getCartQuantity?: (addonId: string) => number;\n /** Update quantity in cart */\n onUpdateQuantity?: (addonId: string, quantity: number) => void;\n /** Remove from cart */\n onRemoveFromCart?: (addonId: string) => void;\n /** Maximum number of addons to show (default: 3) */\n maxAddons?: number;\n isLoading?: boolean;\n className?: string;\n /** Optional tour anchor applied to the section root (for onboarding tours). */\n dataTour?: string;\n}\n\nconst AddonCard: FC<{\n addon: Addon;\n onView: () => void;\n onPurchase: () => void;\n onAddToCart?: () => void;\n isInCart?: boolean;\n cartQuantity?: number;\n onUpdateQuantity?: (quantity: number) => void;\n onRemoveFromCart?: () => void;\n}> = ({\n addon,\n onView,\n onPurchase,\n onAddToCart,\n isInCart = false,\n cartQuantity = 0,\n onUpdateQuantity,\n onRemoveFromCart,\n}) => {\n const [justAdded, setJustAdded] = useState(false);\n\n const handleAddToCart = () => {\n if (onAddToCart) {\n onAddToCart();\n setJustAdded(true);\n setTimeout(() => setJustAdded(false), 2000);\n } else {\n onPurchase();\n }\n };\n\n const formatCurrency = (amount: number, currency: string) => {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: currency || 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(amount);\n };\n\n const getDurationLabel = (duration: PlanDuration) => {\n return duration === 'yearly' ? 'year' : 'month';\n };\n\n // Combine duplicate quotas (e.g., 10 bots + 10 bots = 20 bots)\n const combinedQuotas = useMemo(() => {\n const features = addon.features || [];\n return combineQuotas(features as SubscriptionFeatures[]);\n }, [addon.features]);\n\n return (\n <article className=\"flex flex-col border border-border-seam rounded-card bg-bg-surface shadow-[var(--shadow-pop)] overflow-hidden motion-safe:hover:translate-y-[var(--pop-lift-y)] hover:shadow-elevation-2 [@media(hover:none)]:hover:translate-y-0 transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)]\">\n {/* Header */}\n <header className=\"p-4 border-b border-border-subtle\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-status-success-bg-subtle flex items-center justify-center\">\n <Package className=\"size-5 text-status-success-text\" />\n </div>\n <div>\n <h4 className=\"font-semibold text-text-primary\">{addon.name}</h4>\n <p className=\"text-sm text-text-secondary\">\n {formatCurrency(addon.price, addon.currency)}/{getDurationLabel(addon.duration)}\n </p>\n </div>\n </div>\n </div>\n </header>\n\n {/* Body */}\n <div className=\"p-4 flex-1\">\n {combinedQuotas.length > 0 ? (\n <ul className=\"space-y-2\">\n {combinedQuotas.slice(0, 2).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <span className=\"size-1.5 rounded-full bg-status-success-bg\" />\n <span className=\"text-text-secondary\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 2 && (\n <li className=\"text-xs text-text-secondary\">\n +{combinedQuotas.length - 2} more quotas\n </li>\n )}\n </ul>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Enhance your subscription with additional features\n </p>\n )}\n </div>\n\n {/* Footer */}\n <footer className=\"p-4 border-t border-border-subtle bg-bg-sunken flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={onView}\n className=\"flex-1 px-3 py-2 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors text-center\"\n >\n View Details\n </button>\n {isInCart && onUpdateQuantity ? (\n // Quantity controls when in cart\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={onRemoveFromCart}\n className=\"p-2 text-status-error-text hover:bg-status-error-bg-subtle rounded-button transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(cartQuantity - 1)}\n className=\"p-2 text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-8 text-center font-medium text-text-primary\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(cartQuantity + 1)}\n className=\"p-2 text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n ) : (\n // Add to cart button\n <button\n type=\"button\"\n onClick={handleAddToCart}\n disabled={justAdded}\n className={`flex-1 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-center flex items-center justify-center gap-1.5 ${\n justAdded\n ? 'bg-status-success-bg text-action-primary-text hover:bg-status-success-bg/90'\n : 'text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bgHover'\n }`}\n >\n {justAdded ? (\n <>\n <Check className=\"size-4\" />\n Added\n </>\n ) : (\n <>\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </>\n )}\n </button>\n )}\n </footer>\n </article>\n );\n};\n\nexport const RecommendedAddonsSection: FC<RecommendedAddonsSectionProps> = ({\n addons,\n hasSubscription,\n onViewAddon,\n onPurchaseAddon,\n onViewAllAddons,\n onAddToCart,\n isInCart,\n getCartQuantity,\n onUpdateQuantity,\n onRemoveFromCart,\n maxAddons = 3,\n isLoading = false,\n className = '',\n dataTour,\n}) => {\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 // Only show if user has a subscription\n if (!hasSubscription) {\n return null;\n }\n\n // Limit addons to maxAddons\n const displayAddons = addons.slice(0, maxAddons);\n\n if (isLoading) {\n return (\n <section className={`space-y-4 ${className}`} data-tour={dataTour}>\n <header>\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.overview.recommendedAddons', 'Recommended Add-ons')}\n </h3>\n <p className=\"text-sm text-text-secondary\">\n Enhance your subscription with these popular add-ons\n </p>\n </header>\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"size-6 animate-spin text-text-secondary\" />\n </div>\n </section>\n );\n }\n\n if (displayAddons.length === 0) {\n return null;\n }\n\n return (\n <section className={`space-y-4 ${className}`} data-tour={dataTour}>\n {/* Header */}\n <header className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Sparkles className=\"size-5 text-status-warning-text\" />\n <div>\n <h3 className=\"text-lg font-semibold text-text-primary\">Recommended Add-ons</h3>\n <p className=\"text-sm text-text-secondary\">\n Enhance your subscription with these popular add-ons\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onViewAllAddons}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-link hover:text-text-link transition-colors\"\n >\n View All\n <ChevronRight className=\"size-4\" />\n </button>\n </header>\n\n {/* Addons Grid - Limited to maxAddons */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {displayAddons.map((addon) => (\n <AddonCard\n key={addon.id}\n addon={addon}\n onView={() => onViewAddon(addon.id)}\n onPurchase={() => onPurchaseAddon(addon.id)}\n onAddToCart={onAddToCart ? () => onAddToCart(addon) : undefined}\n isInCart={isInCart ? isInCart(addon.id) : false}\n cartQuantity={getCartQuantity ? getCartQuantity(addon.id) : 0}\n onUpdateQuantity={\n onUpdateQuantity ? (qty) => onUpdateQuantity(addon.id, qty) : undefined\n }\n onRemoveFromCart={onRemoveFromCart ? () => onRemoveFromCart(addon.id) : undefined}\n />\n ))}\n </div>\n\n {/* See More Link - show if there are more addons */}\n {addons.length > maxAddons && (\n <div className=\"flex justify-center pt-2\">\n <button\n type=\"button\"\n onClick={onViewAllAddons}\n className=\"inline-flex items-center gap-2 text-sm font-medium text-text-secondary hover:text-text-primary transition-colors\"\n >\n View all {addons.length} add-ons\n <ArrowRight className=\"size-4\" />\n </button>\n </div>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RecommendedAddonsSection.js","names":[],"sources":["../../../../../src/billing/modules/dashboard/components/RecommendedAddonsSection.tsx"],"sourcesContent":["/**\n * Recommended Addons Section Component\n * Displays recommended addons that users might want to purchase\n * Supports add to cart functionality\n */\n\nimport { type FC, useState, useMemo } from 'react';\nimport {\n Package,\n ArrowRight,\n Sparkles,\n ChevronRight,\n Loader2,\n ShoppingCart,\n Check,\n Plus,\n Minus,\n Trash2,\n} from 'lucide-react';\nimport type { Addon, PlanDuration, SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { getPlanFeatureQuantity } from '../../../shared/utils/planFeatureQuantity';\n\n/**\n * Combined quota info for display\n */\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\n/**\n * Combines duplicate quotas by quota ID and aggregates their values\n * e.g., 10 bots + 10 bots = 20 bots (shown as one feature)\n */\nfunction combineQuotas(features: SubscriptionFeatures[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n\n for (const feature of features) {\n if (!feature.quota) continue;\n\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n}\n\ninterface RecommendedAddonsSectionProps {\n addons: Addon[];\n hasSubscription: boolean;\n onViewAddon: (addonId: string) => void;\n onPurchaseAddon: (addonId: string) => void;\n onViewAllAddons: () => void;\n /** Handler for adding addon to cart */\n onAddToCart?: (addon: Addon) => void;\n /** Check if addon is in cart */\n isInCart?: (addonId: string) => boolean;\n /** Get cart quantity for addon */\n getCartQuantity?: (addonId: string) => number;\n /** Update quantity in cart */\n onUpdateQuantity?: (addonId: string, quantity: number) => void;\n /** Remove from cart */\n onRemoveFromCart?: (addonId: string) => void;\n /** Maximum number of addons to show (default: 3) */\n maxAddons?: number;\n isLoading?: boolean;\n className?: string;\n /** Optional tour anchor applied to the section root (for onboarding tours). */\n dataTour?: string;\n}\n\nconst AddonCard: FC<{\n addon: Addon;\n onView: () => void;\n onPurchase: () => void;\n onAddToCart?: () => void;\n isInCart?: boolean;\n cartQuantity?: number;\n onUpdateQuantity?: (quantity: number) => void;\n onRemoveFromCart?: () => void;\n}> = ({\n addon,\n onView,\n onPurchase,\n onAddToCart,\n isInCart = false,\n cartQuantity = 0,\n onUpdateQuantity,\n onRemoveFromCart,\n}) => {\n const [justAdded, setJustAdded] = useState(false);\n\n const handleAddToCart = () => {\n if (onAddToCart) {\n onAddToCart();\n setJustAdded(true);\n setTimeout(() => setJustAdded(false), 2000);\n } else {\n onPurchase();\n }\n };\n\n const formatCurrency = (amount: number, currency: string) => {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: currency || 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(amount);\n };\n\n const getDurationLabel = (duration: PlanDuration) => {\n return duration === 'yearly' ? 'year' : 'month';\n };\n\n // Combine duplicate quotas (e.g., 10 bots + 10 bots = 20 bots)\n const combinedQuotas = useMemo(() => {\n const features = addon.features || [];\n return combineQuotas(features as SubscriptionFeatures[]);\n }, [addon.features]);\n\n return (\n <article className=\"flex flex-col border border-border-seam rounded-card bg-bg-surface shadow-[var(--shadow-pop)] overflow-hidden hover:shadow-elevation-2 [@media(hover:none)]: transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)]\">\n {/* Header */}\n <header className=\"p-4 border-b border-border-subtle\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-status-success-bg-subtle flex items-center justify-center\">\n <Package className=\"size-5 text-status-success-text\" />\n </div>\n <div>\n <h4 className=\"font-semibold text-text-primary\">{addon.name}</h4>\n <p className=\"text-sm text-text-secondary\">\n {formatCurrency(addon.price, addon.currency)}/{getDurationLabel(addon.duration)}\n </p>\n </div>\n </div>\n </div>\n </header>\n\n {/* Body */}\n <div className=\"p-4 flex-1\">\n {combinedQuotas.length > 0 ? (\n <ul className=\"space-y-2\">\n {combinedQuotas.slice(0, 2).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <span className=\"size-1.5 rounded-full bg-status-success-bg\" />\n <span className=\"text-text-secondary\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 2 && (\n <li className=\"text-xs text-text-secondary\">\n +{combinedQuotas.length - 2} more quotas\n </li>\n )}\n </ul>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Enhance your subscription with additional features\n </p>\n )}\n </div>\n\n {/* Footer */}\n <footer className=\"p-4 border-t border-border-subtle bg-bg-sunken flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={onView}\n className=\"flex-1 px-3 py-2 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors text-center\"\n >\n View Details\n </button>\n {isInCart && onUpdateQuantity ? (\n // Quantity controls when in cart\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={onRemoveFromCart}\n className=\"p-2 text-status-error-text hover:bg-status-error-bg-subtle rounded-button transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(cartQuantity - 1)}\n className=\"p-2 text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-8 text-center font-medium text-text-primary\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(cartQuantity + 1)}\n className=\"p-2 text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n ) : (\n // Add to cart button\n <button\n type=\"button\"\n onClick={handleAddToCart}\n disabled={justAdded}\n className={`flex-1 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-center flex items-center justify-center gap-1.5 ${\n justAdded\n ? 'bg-status-success-bg text-action-primary-text hover:bg-status-success-bg/90'\n : 'text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bgHover'\n }`}\n >\n {justAdded ? (\n <>\n <Check className=\"size-4\" />\n Added\n </>\n ) : (\n <>\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </>\n )}\n </button>\n )}\n </footer>\n </article>\n );\n};\n\nexport const RecommendedAddonsSection: FC<RecommendedAddonsSectionProps> = ({\n addons,\n hasSubscription,\n onViewAddon,\n onPurchaseAddon,\n onViewAllAddons,\n onAddToCart,\n isInCart,\n getCartQuantity,\n onUpdateQuantity,\n onRemoveFromCart,\n maxAddons = 3,\n isLoading = false,\n className = '',\n dataTour,\n}) => {\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 // Only show if user has a subscription\n if (!hasSubscription) {\n return null;\n }\n\n // Limit addons to maxAddons\n const displayAddons = addons.slice(0, maxAddons);\n\n if (isLoading) {\n return (\n <section className={`space-y-4 ${className}`} data-tour={dataTour}>\n <header>\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.overview.recommendedAddons', 'Recommended Add-ons')}\n </h3>\n <p className=\"text-sm text-text-secondary\">\n Enhance your subscription with these popular add-ons\n </p>\n </header>\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"size-6 animate-spin text-text-secondary\" />\n </div>\n </section>\n );\n }\n\n if (displayAddons.length === 0) {\n return null;\n }\n\n return (\n <section className={`space-y-4 ${className}`} data-tour={dataTour}>\n {/* Header */}\n <header className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Sparkles className=\"size-5 text-status-warning-text\" />\n <div>\n <h3 className=\"text-lg font-semibold text-text-primary\">Recommended Add-ons</h3>\n <p className=\"text-sm text-text-secondary\">\n Enhance your subscription with these popular add-ons\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onViewAllAddons}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-link hover:text-text-link transition-colors\"\n >\n View All\n <ChevronRight className=\"size-4\" />\n </button>\n </header>\n\n {/* Addons Grid - Limited to maxAddons */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {displayAddons.map((addon) => (\n <AddonCard\n key={addon.id}\n addon={addon}\n onView={() => onViewAddon(addon.id)}\n onPurchase={() => onPurchaseAddon(addon.id)}\n onAddToCart={onAddToCart ? () => onAddToCart(addon) : undefined}\n isInCart={isInCart ? isInCart(addon.id) : false}\n cartQuantity={getCartQuantity ? getCartQuantity(addon.id) : 0}\n onUpdateQuantity={\n onUpdateQuantity ? (qty) => onUpdateQuantity(addon.id, qty) : undefined\n }\n onRemoveFromCart={onRemoveFromCart ? () => onRemoveFromCart(addon.id) : undefined}\n />\n ))}\n </div>\n\n {/* See More Link - show if there are more addons */}\n {addons.length > maxAddons && (\n <div className=\"flex justify-center pt-2\">\n <button\n type=\"button\"\n onClick={onViewAllAddons}\n className=\"inline-flex items-center gap-2 text-sm font-medium text-text-secondary hover:text-text-primary transition-colors\"\n >\n View all {addons.length} add-ons\n <ArrowRight className=\"size-4\" />\n </button>\n </div>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
|
|
@@ -363,7 +363,7 @@ var w = () => {
|
|
|
363
363
|
onClick: o,
|
|
364
364
|
className: "group inline-flex w-full items-center justify-center gap-2 rounded-button bg-action-primary-bg px-4 py-2.5 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
365
365
|
children: ["Subscribe", /* @__PURE__ */ y(g, {
|
|
366
|
-
className: "size-4 transition-transform duration-200
|
|
366
|
+
className: "size-4 transition-transform duration-200",
|
|
367
367
|
"aria-hidden": "true"
|
|
368
368
|
})]
|
|
369
369
|
}), /* @__PURE__ */ y("button", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlansBrowsePage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlansBrowsePage.tsx"],"sourcesContent":["/**\n * Plans Module - Plans Browse Page (User-facing)\n * Browse available plans and subscribe. No create/edit/toggle actions.\n */\n\nimport { useState, type FC } from 'react';\nimport { Check, ArrowRight, Sparkles } from 'lucide-react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { usePlans } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n getPlanFeatureQuantity,\n isServerError,\n} from '../../../shared/utils';\nimport { PlanDuration } from '../../../shared/types';\nimport type { Plan, PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n useDefaultBillingCurrency,\n getDisplayPrice,\n} from '../../../hooks/useDefaultBillingCurrency';\nimport {\n FEATURED_QUOTA_NAMES,\n FEATURED_QUOTA_LABELS,\n formatFeaturedQuotaValue,\n} from '../constants/featuredQuotas';\n\ntype ViewMode = 'grid' | 'list';\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\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 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: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\n/** Returns only the featured quotas, in the canonical display order. */\nfunction getFeaturedQuotas(features: PlanFeature[]): CombinedQuota[] {\n const all = combineQuotas(features);\n const byName = new Map(all.map((q) => [q.name, q]));\n return FEATURED_QUOTA_NAMES.flatMap((name) => {\n const q = byName.get(name);\n return q ? [q] : [];\n });\n}\n\nexport const PlansBrowsePage: 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 { productId } = useBilling();\n const { plans, isLoading, error, refetch } = usePlans({\n includeFree: false,\n productId: productId ?? undefined,\n });\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [billingInterval, setBillingInterval] = useState<PlanDuration>(PlanDuration.MONTHLY);\n\n // Only show active, publicly purchasable plans to users\n const filteredPlans = plans.filter(\n (plan) => plan.isActive && plan.isPurchasable !== false && plan.duration === billingInterval\n );\n\n // Spotlight a recommended tier (the second plan when there are 2+, the common\n // \"mid / Pro\" sweet-spot) so the eye lands on a clear primary choice.\n const recommendedIndex = filteredPlans.length >= 2 ? 1 : -1;\n\n if (isLoading && plans.length === 0) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded-card\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 p-5 space-y-4\"\n >\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\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.unableToLoadPlans', 'Unable to load plans.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.plans.browsePlansTitle', 'Plans')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.plans.browsePlansSubtitle', 'Choose a plan that fits your needs')}\n </p>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken\">\n {([PlanDuration.MONTHLY, PlanDuration.YEARLY] as const).map((interval) => (\n <button\n type=\"button\"\n key={interval}\n onClick={() => setBillingInterval(interval)}\n className={`px-3 py-1.5 text-sm font-medium rounded-button transition-colors ${\n billingInterval === interval\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {interval === PlanDuration.MONTHLY ? 'Monthly' : 'Yearly'}\n </button>\n ))}\n </div>\n\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-button transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-button transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Plans */}\n {filteredPlans.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border-seam rounded-card\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-text-primary\">\n {tr('billing.plans.noPlansAvailable', 'No plans available')}\n </h3>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.plans.checkBackLater', 'Check back later for new plans.')}\n </p>\n </div>\n ) : viewMode === 'grid' ? (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-stretch\">\n {filteredPlans.map((plan, idx) => (\n <BrowsePlanCard\n key={plan.id}\n plan={plan}\n highlighted={idx === recommendedIndex}\n onView={() => navigateTo(`/plans/${plan.id}`)}\n onSubscribe={() => navigateTo(`/checkout/${plan.id}`)}\n />\n ))}\n <EnterpriseCard />\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredPlans.map((plan) => (\n <BrowsePlanRow\n key={plan.id}\n plan={plan}\n onView={() => navigateTo(`/plans/${plan.id}`)}\n onSubscribe={() => navigateTo(`/checkout/${plan.id}`)}\n />\n ))}\n <EnterpriseRow />\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Static Enterprise \"Contact Us\" card — no backend plan record\n// ============================================================================\n\nconst ENTERPRISE_HIGHLIGHTS = [\n 'Unlimited Agents & Sessions',\n 'Dedicated single-tenant infra',\n 'Customer-managed encryption keys',\n 'SSO / SCIM provisioning',\n 'Custom SLA & support contract',\n 'Volume-based custom pricing',\n 'Audit log with custom retention',\n 'Priority onboarding & migration',\n];\n\nconst EnterpriseCard: FC = () => (\n <div className=\"border border-border-strong bg-[var(--color-accent-soft)] rounded-card shadow-elevation-1 p-5 flex flex-col h-full\">\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-lg font-semibold text-text-primary\">Enterprise</h3>\n <span className=\"text-xs font-medium px-2 py-0.5 rounded-full bg-bg-surface text-text-link\">\n Contact Us\n </span>\n </div>\n <span className=\"text-sm text-text-secondary\">Custom contract</span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-semibold text-text-primary\">Custom pricing</span>\n </div>\n\n <ul className=\"space-y-2 mb-4 flex-1\">\n {ENTERPRISE_HIGHLIGHTS.map((item) => (\n <li key={item} className=\"flex items-center gap-2 text-sm\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n <span className=\"text-text-primary\">{item}</span>\n </li>\n ))}\n </ul>\n\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n <a\n href=\"mailto:contact@burdenoff.com?subject=Enterprise Plan Inquiry\"\n className=\"block w-full px-4 py-2 text-sm font-medium text-center bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Contact Us\n </a>\n </div>\n </div>\n);\n\nconst EnterpriseRow: FC = () => (\n <div className=\"flex items-center gap-4 p-5 border border-border-strong rounded-card bg-[var(--color-accent-soft)] shadow-elevation-1 transition-all duration-200 hover:shadow-elevation-2\">\n <div className=\"size-10 rounded-lg bg-bg-surface flex items-center justify-center shrink-0 shadow-elevation-1\">\n <svg className=\"size-5 text-text-link\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-2 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-text-primary\">Enterprise</h3>\n <span className=\"text-xs font-medium px-2 py-0.5 rounded-full bg-bg-surface text-text-link shrink-0\">\n Contact Us\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">Dedicated infra · SSO/SCIM · Custom SLA</p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">Custom pricing</div>\n <div className=\"text-sm text-text-secondary\">contract-based</div>\n </div>\n\n <a\n href=\"mailto:contact@burdenoff.com?subject=Enterprise Plan Inquiry\"\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200 shrink-0\"\n >\n Contact Us\n </a>\n </div>\n);\n\n// ============================================================================\n// Browse Plan Card (no edit/toggle)\n// ============================================================================\n\nconst BrowsePlanCard: FC<{\n plan: Plan;\n highlighted?: boolean;\n onView: () => void;\n onSubscribe: () => void;\n}> = ({ plan, highlighted = false, onView, onSubscribe }) => {\n const featuredQuotas = getFeaturedQuotas(plan.features || []);\n const allQuotas = combineQuotas(plan.features || []);\n const extraCount = allQuotas.length - featuredQuotas.length;\n const displayCurrency = useDefaultBillingCurrency();\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n\n return (\n <div\n className={`relative flex flex-col h-full rounded-card bg-bg-surface p-6 transition-[border-color,box-shadow] duration-200 ${\n highlighted\n ? 'border-2 border-action-primary-bg shadow-[var(--shadow-glow)]'\n : 'border border-border-subtle shadow-elevation-1 hover:border-border-strong hover:shadow-elevation-2'\n }`}\n >\n {highlighted && (\n <span className=\"absolute -top-3 left-1/2 -translate-x-1/2 inline-flex items-center gap-1 rounded-full bg-action-primary-bg px-3 py-1 text-xs font-semibold text-action-primary-text shadow-elevation-2\">\n <Sparkles className=\"size-3.5\" aria-hidden=\"true\" />\n Most popular\n </span>\n )}\n\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-semibold text-text-primary\">{plan.name}</h3>\n <span className=\"text-sm text-text-secondary\">\n {formatPlanDurationLabel(plan.duration)}\n </span>\n </div>\n\n <div className=\"flex items-baseline gap-1.5 mb-5\">\n <span className=\"text-4xl font-bold tabular-nums tracking-tight text-text-primary\">\n {formatCurrency(price, currency)}\n </span>\n <span className=\"text-text-secondary text-sm\">/ {formatPlanDuration(plan.duration)}</span>\n </div>\n\n {featuredQuotas.length > 0 && (\n <ul className=\"space-y-2.5 mb-5 flex-1\">\n {featuredQuotas.map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between gap-2 text-sm\">\n <span className=\"flex items-center gap-2 min-w-0\">\n <span className=\"flex size-5 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]\">\n <Check className=\"size-3 text-action-primary-bg\" aria-hidden=\"true\" />\n </span>\n <span className=\"truncate text-text-primary\">\n {FEATURED_QUOTA_LABELS[quota.name] ?? quota.name}\n </span>\n </span>\n <span className=\"font-semibold text-text-primary tabular-nums shrink-0\">\n {formatFeaturedQuotaValue(quota.name, quota.totalValue)}\n </span>\n </li>\n ))}\n {extraCount > 0 && (\n <li className=\"text-sm text-text-secondary pl-7\">+{extraCount} more — see details</li>\n )}\n </ul>\n )}\n\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n <button\n type=\"button\"\n onClick={onSubscribe}\n className=\"group inline-flex w-full items-center justify-center gap-2 rounded-button bg-action-primary-bg px-4 py-2.5 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n Subscribe\n <ArrowRight\n className=\"size-4 transition-transform duration-200 group-hover:translate-x-0.5\"\n aria-hidden=\"true\"\n />\n </button>\n <button\n type=\"button\"\n onClick={onView}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-secondary rounded-button hover:bg-bg-sunken hover:text-text-primary hover:border-border-strong transition-colors duration-200\"\n >\n View details\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Browse Plan Row (no edit/toggle)\n// ============================================================================\n\nconst BrowsePlanRow: FC<{ plan: Plan; onView: () => void; onSubscribe: () => void }> = ({\n plan,\n onView,\n onSubscribe,\n}) => {\n const featuredQuotas = getFeaturedQuotas(plan.features || []);\n const displayCurrency = useDefaultBillingCurrency();\n\n return (\n <div className=\"flex items-center gap-4 p-5 border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 transition-all duration-200 hover:border-border-strong hover:shadow-elevation-2\">\n <div className=\"size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center shrink-0\">\n <svg\n className=\"size-5 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <h3 className=\"font-semibold text-text-primary truncate\">{plan.name}</h3>\n <p className=\"text-sm text-text-secondary\">\n {featuredQuotas.length} key feature{featuredQuotas.length !== 1 ? 's' : ''} included\n </p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">\n {(() => {\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-text-secondary\">per {formatPlanDuration(plan.duration)}</div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={onSubscribe}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Subscribe\n </button>\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken hover:border-border-strong transition-all duration-200\"\n >\n View\n </button>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlansBrowsePage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlansBrowsePage.tsx"],"sourcesContent":["/**\n * Plans Module - Plans Browse Page (User-facing)\n * Browse available plans and subscribe. No create/edit/toggle actions.\n */\n\nimport { useState, type FC } from 'react';\nimport { Check, ArrowRight, Sparkles } from 'lucide-react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { usePlans } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n getPlanFeatureQuantity,\n isServerError,\n} from '../../../shared/utils';\nimport { PlanDuration } from '../../../shared/types';\nimport type { Plan, PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n useDefaultBillingCurrency,\n getDisplayPrice,\n} from '../../../hooks/useDefaultBillingCurrency';\nimport {\n FEATURED_QUOTA_NAMES,\n FEATURED_QUOTA_LABELS,\n formatFeaturedQuotaValue,\n} from '../constants/featuredQuotas';\n\ntype ViewMode = 'grid' | 'list';\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\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 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: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\n/** Returns only the featured quotas, in the canonical display order. */\nfunction getFeaturedQuotas(features: PlanFeature[]): CombinedQuota[] {\n const all = combineQuotas(features);\n const byName = new Map(all.map((q) => [q.name, q]));\n return FEATURED_QUOTA_NAMES.flatMap((name) => {\n const q = byName.get(name);\n return q ? [q] : [];\n });\n}\n\nexport const PlansBrowsePage: 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 { productId } = useBilling();\n const { plans, isLoading, error, refetch } = usePlans({\n includeFree: false,\n productId: productId ?? undefined,\n });\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [billingInterval, setBillingInterval] = useState<PlanDuration>(PlanDuration.MONTHLY);\n\n // Only show active, publicly purchasable plans to users\n const filteredPlans = plans.filter(\n (plan) => plan.isActive && plan.isPurchasable !== false && plan.duration === billingInterval\n );\n\n // Spotlight a recommended tier (the second plan when there are 2+, the common\n // \"mid / Pro\" sweet-spot) so the eye lands on a clear primary choice.\n const recommendedIndex = filteredPlans.length >= 2 ? 1 : -1;\n\n if (isLoading && plans.length === 0) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded-card\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 p-5 space-y-4\"\n >\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\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.unableToLoadPlans', 'Unable to load plans.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.plans.browsePlansTitle', 'Plans')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.plans.browsePlansSubtitle', 'Choose a plan that fits your needs')}\n </p>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken\">\n {([PlanDuration.MONTHLY, PlanDuration.YEARLY] as const).map((interval) => (\n <button\n type=\"button\"\n key={interval}\n onClick={() => setBillingInterval(interval)}\n className={`px-3 py-1.5 text-sm font-medium rounded-button transition-colors ${\n billingInterval === interval\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {interval === PlanDuration.MONTHLY ? 'Monthly' : 'Yearly'}\n </button>\n ))}\n </div>\n\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-button transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-button transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Plans */}\n {filteredPlans.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border-seam rounded-card\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-text-primary\">\n {tr('billing.plans.noPlansAvailable', 'No plans available')}\n </h3>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.plans.checkBackLater', 'Check back later for new plans.')}\n </p>\n </div>\n ) : viewMode === 'grid' ? (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-stretch\">\n {filteredPlans.map((plan, idx) => (\n <BrowsePlanCard\n key={plan.id}\n plan={plan}\n highlighted={idx === recommendedIndex}\n onView={() => navigateTo(`/plans/${plan.id}`)}\n onSubscribe={() => navigateTo(`/checkout/${plan.id}`)}\n />\n ))}\n <EnterpriseCard />\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredPlans.map((plan) => (\n <BrowsePlanRow\n key={plan.id}\n plan={plan}\n onView={() => navigateTo(`/plans/${plan.id}`)}\n onSubscribe={() => navigateTo(`/checkout/${plan.id}`)}\n />\n ))}\n <EnterpriseRow />\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Static Enterprise \"Contact Us\" card — no backend plan record\n// ============================================================================\n\nconst ENTERPRISE_HIGHLIGHTS = [\n 'Unlimited Agents & Sessions',\n 'Dedicated single-tenant infra',\n 'Customer-managed encryption keys',\n 'SSO / SCIM provisioning',\n 'Custom SLA & support contract',\n 'Volume-based custom pricing',\n 'Audit log with custom retention',\n 'Priority onboarding & migration',\n];\n\nconst EnterpriseCard: FC = () => (\n <div className=\"border border-border-strong bg-[var(--color-accent-soft)] rounded-card shadow-elevation-1 p-5 flex flex-col h-full\">\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-lg font-semibold text-text-primary\">Enterprise</h3>\n <span className=\"text-xs font-medium px-2 py-0.5 rounded-full bg-bg-surface text-text-link\">\n Contact Us\n </span>\n </div>\n <span className=\"text-sm text-text-secondary\">Custom contract</span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-semibold text-text-primary\">Custom pricing</span>\n </div>\n\n <ul className=\"space-y-2 mb-4 flex-1\">\n {ENTERPRISE_HIGHLIGHTS.map((item) => (\n <li key={item} className=\"flex items-center gap-2 text-sm\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n <span className=\"text-text-primary\">{item}</span>\n </li>\n ))}\n </ul>\n\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n <a\n href=\"mailto:contact@burdenoff.com?subject=Enterprise Plan Inquiry\"\n className=\"block w-full px-4 py-2 text-sm font-medium text-center bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Contact Us\n </a>\n </div>\n </div>\n);\n\nconst EnterpriseRow: FC = () => (\n <div className=\"flex items-center gap-4 p-5 border border-border-strong rounded-card bg-[var(--color-accent-soft)] shadow-elevation-1 transition-all duration-200 hover:shadow-elevation-2\">\n <div className=\"size-10 rounded-lg bg-bg-surface flex items-center justify-center shrink-0 shadow-elevation-1\">\n <svg className=\"size-5 text-text-link\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-2 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-text-primary\">Enterprise</h3>\n <span className=\"text-xs font-medium px-2 py-0.5 rounded-full bg-bg-surface text-text-link shrink-0\">\n Contact Us\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">Dedicated infra · SSO/SCIM · Custom SLA</p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">Custom pricing</div>\n <div className=\"text-sm text-text-secondary\">contract-based</div>\n </div>\n\n <a\n href=\"mailto:contact@burdenoff.com?subject=Enterprise Plan Inquiry\"\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200 shrink-0\"\n >\n Contact Us\n </a>\n </div>\n);\n\n// ============================================================================\n// Browse Plan Card (no edit/toggle)\n// ============================================================================\n\nconst BrowsePlanCard: FC<{\n plan: Plan;\n highlighted?: boolean;\n onView: () => void;\n onSubscribe: () => void;\n}> = ({ plan, highlighted = false, onView, onSubscribe }) => {\n const featuredQuotas = getFeaturedQuotas(plan.features || []);\n const allQuotas = combineQuotas(plan.features || []);\n const extraCount = allQuotas.length - featuredQuotas.length;\n const displayCurrency = useDefaultBillingCurrency();\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n\n return (\n <div\n className={`relative flex flex-col h-full rounded-card bg-bg-surface p-6 transition-[border-color,box-shadow] duration-200 ${\n highlighted\n ? 'border-2 border-action-primary-bg shadow-[var(--shadow-glow)]'\n : 'border border-border-subtle shadow-elevation-1 hover:border-border-strong hover:shadow-elevation-2'\n }`}\n >\n {highlighted && (\n <span className=\"absolute -top-3 left-1/2 -translate-x-1/2 inline-flex items-center gap-1 rounded-full bg-action-primary-bg px-3 py-1 text-xs font-semibold text-action-primary-text shadow-elevation-2\">\n <Sparkles className=\"size-3.5\" aria-hidden=\"true\" />\n Most popular\n </span>\n )}\n\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-semibold text-text-primary\">{plan.name}</h3>\n <span className=\"text-sm text-text-secondary\">\n {formatPlanDurationLabel(plan.duration)}\n </span>\n </div>\n\n <div className=\"flex items-baseline gap-1.5 mb-5\">\n <span className=\"text-4xl font-bold tabular-nums tracking-tight text-text-primary\">\n {formatCurrency(price, currency)}\n </span>\n <span className=\"text-text-secondary text-sm\">/ {formatPlanDuration(plan.duration)}</span>\n </div>\n\n {featuredQuotas.length > 0 && (\n <ul className=\"space-y-2.5 mb-5 flex-1\">\n {featuredQuotas.map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between gap-2 text-sm\">\n <span className=\"flex items-center gap-2 min-w-0\">\n <span className=\"flex size-5 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]\">\n <Check className=\"size-3 text-action-primary-bg\" aria-hidden=\"true\" />\n </span>\n <span className=\"truncate text-text-primary\">\n {FEATURED_QUOTA_LABELS[quota.name] ?? quota.name}\n </span>\n </span>\n <span className=\"font-semibold text-text-primary tabular-nums shrink-0\">\n {formatFeaturedQuotaValue(quota.name, quota.totalValue)}\n </span>\n </li>\n ))}\n {extraCount > 0 && (\n <li className=\"text-sm text-text-secondary pl-7\">+{extraCount} more — see details</li>\n )}\n </ul>\n )}\n\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n <button\n type=\"button\"\n onClick={onSubscribe}\n className=\"group inline-flex w-full items-center justify-center gap-2 rounded-button bg-action-primary-bg px-4 py-2.5 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n Subscribe\n <ArrowRight\n className=\"size-4 transition-transform duration-200\"\n aria-hidden=\"true\"\n />\n </button>\n <button\n type=\"button\"\n onClick={onView}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-secondary rounded-button hover:bg-bg-sunken hover:text-text-primary hover:border-border-strong transition-colors duration-200\"\n >\n View details\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Browse Plan Row (no edit/toggle)\n// ============================================================================\n\nconst BrowsePlanRow: FC<{ plan: Plan; onView: () => void; onSubscribe: () => void }> = ({\n plan,\n onView,\n onSubscribe,\n}) => {\n const featuredQuotas = getFeaturedQuotas(plan.features || []);\n const displayCurrency = useDefaultBillingCurrency();\n\n return (\n <div className=\"flex items-center gap-4 p-5 border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 transition-all duration-200 hover:border-border-strong hover:shadow-elevation-2\">\n <div className=\"size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center shrink-0\">\n <svg\n className=\"size-5 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <h3 className=\"font-semibold text-text-primary truncate\">{plan.name}</h3>\n <p className=\"text-sm text-text-secondary\">\n {featuredQuotas.length} key feature{featuredQuotas.length !== 1 ? 's' : ''} included\n </p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">\n {(() => {\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-text-secondary\">per {formatPlanDuration(plan.duration)}</div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={onSubscribe}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Subscribe\n </button>\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken hover:border-border-strong transition-all duration-200\"\n >\n View\n </button>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -282,7 +282,7 @@ var b = () => {
|
|
|
282
282
|
}, x = ({ plan: e, onSelect: t, onView: o, onToggle: s, canSelect: c, canManage: l, isToggling: f }) => {
|
|
283
283
|
let p = d(), h = m(() => y(e.features || []), [e.features]);
|
|
284
284
|
return /* @__PURE__ */ _("div", {
|
|
285
|
-
className: `relative border rounded-card p-6 flex flex-col h-full transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] ${e.isActive ? "border-border-seam bg-bg-surface shadow-[var(--shadow-pop)]
|
|
285
|
+
className: `relative border rounded-card p-6 flex flex-col h-full transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] ${e.isActive ? "border-border-seam bg-bg-surface shadow-[var(--shadow-pop)] hover:shadow-[var(--shadow-elevation-2)] [@media(hover:none)]:" : "border-dashed border-text-muted/30 bg-bg-sunken/30 shadow-elevation-1"}`,
|
|
286
286
|
children: [
|
|
287
287
|
!e.isActive && /* @__PURE__ */ g("span", {
|
|
288
288
|
className: "absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-bg-sunken text-text-muted rounded",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlansListPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlansListPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plans List Page\n * Displays all available pricing plans\n */\n\nimport { useState, useMemo, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlans, usePlanMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n getPlanFeatureQuantity,\n isServerError,\n} from '../../../shared/utils';\nimport type { Plan, PlanDuration, PlanFeature } from '../../../shared/types';\nimport { PricingModel } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n useDefaultBillingCurrency,\n getDisplayPrice,\n} from '../../../hooks/useDefaultBillingCurrency';\n\ntype ViewMode = 'grid' | 'list';\n\n/**\n * Combined quota info for display\n */\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\n/**\n * Combines duplicate quotas by quota ID and aggregates their values\n */\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n\n for (const feature of features) {\n if (!feature.quota) continue;\n\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n}\n\nexport const PlansListPage: 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 { plans, isLoading, error, refetch } = usePlans();\n const { togglePlan, isToggling } = usePlanMutations();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [showInactive, setShowInactive] = useState(false);\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n\n // Permission check\n if (!permissions.canViewPlans) {\n return (\n <AccessDenied\n message={tr(\n 'billing.plans.noViewPermission',\n \"You don't have permission to view pricing plans.\"\n )}\n />\n );\n }\n\n // Filter plans\n const filteredPlans = plans.filter((plan) => {\n if (!showInactive && !plan.isActive) return false;\n if (billingInterval !== 'all' && plan.duration !== billingInterval) return false;\n return true;\n });\n\n const handleTogglePlan = async (plan: Plan) => {\n if (!permissions.canManagePlans) return;\n try {\n await togglePlan(plan.id, !plan.isActive);\n await refetch();\n } catch (err) {\n console.error('Failed to toggle plan:', err);\n }\n };\n\n const handleSelectPlan = (plan: Plan) => {\n navigateTo(`/checkout/${plan.id}`);\n };\n\n const handleViewPlan = (plan: Plan) => {\n navigateTo(`/plans/${plan.id}`);\n };\n\n // Loading state\n if (isLoading && plans.length === 0) {\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=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} 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-8 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3, 4].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state - show server error component for 500 errors\n if (error) {\n if (isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr(\n 'billing.plans.serverUnavailableMessage',\n 'Unable to load pricing plans. The server might be down or experiencing issues.'\n )}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n // Generic error for non-server errors\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-6 text-status-error-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.failedToLoad', 'Failed to load plans')}\n </h2>\n <p className=\"text-sm text-text-muted\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.plans.title', 'Pricing Plans')}\n </h1>\n <p className=\"text-sm text-text-muted mt-1\">\n {tr('billing.plans.subtitle', 'Choose the plan that best fits your needs')}\n </p>\n </div>\n\n {permissions.canManagePlans && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans/new')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 4v16m8-8H4\"\n />\n </svg>\n Create Plan\n </button>\n )}\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n {/* Billing Interval Toggle */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setBillingInterval('all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === 'all'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n All\n </button>\n <button\n type=\"button\"\n onClick={() => setBillingInterval('monthly' as PlanDuration)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === 'monthly'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n Monthly\n </button>\n <button\n type=\"button\"\n onClick={() => setBillingInterval('yearly' as PlanDuration)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === 'yearly'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n Yearly\n </button>\n </div>\n\n {/* Show Inactive Toggle */}\n {permissions.canManagePlans && (\n <label className=\"flex items-center gap-2 text-sm cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <span className=\"text-text-muted\">\n {tr('billing.plans.showInactive', 'Show inactive plans')}\n </span>\n </label>\n )}\n\n {/* View Mode Toggle */}\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Plans Grid/List */}\n {filteredPlans.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-16 border border-dashed border-border-seam rounded-card bg-bg-surface\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-text-muted\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-text-primary\">\n {tr('billing.plans.noPlansFound', 'No plans found')}\n </h3>\n <p className=\"text-sm text-text-muted mt-1\">\n {showInactive\n ? tr('billing.plans.noPlansFiltered', 'No plans match your current filters.')\n : tr('billing.plans.noActivePlans', 'There are no active plans available.')}\n </p>\n {permissions.canManagePlans && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans/new')}\n className=\"mt-4 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 {tr('billing.plans.createFirstPlan', 'Create your first plan')}\n </button>\n )}\n </div>\n ) : viewMode === 'grid' ? (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {filteredPlans.map((plan) => (\n <PlanCard\n key={plan.id}\n plan={plan}\n onSelect={handleSelectPlan}\n onView={handleViewPlan}\n onToggle={handleTogglePlan}\n canSelect={permissions.canCreateSubscription}\n canManage={permissions.canManagePlans}\n isToggling={isToggling}\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredPlans.map((plan) => (\n <PlanRow\n key={plan.id}\n plan={plan}\n onSelect={handleSelectPlan}\n onView={handleViewPlan}\n onToggle={handleTogglePlan}\n canSelect={permissions.canCreateSubscription}\n canManage={permissions.canManagePlans}\n isToggling={isToggling}\n />\n ))}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Plan Card Component\n// ============================================================================\n\ninterface PlanCardProps {\n plan: Plan;\n onSelect: (plan: Plan) => void;\n onView: (plan: Plan) => void;\n onToggle: (plan: Plan) => void;\n canSelect: boolean;\n canManage: boolean;\n isToggling: boolean;\n}\n\nconst PlanCard: FC<PlanCardProps> = ({\n plan,\n onSelect,\n onView,\n onToggle,\n canSelect,\n canManage,\n isToggling,\n}) => {\n const displayCurrency = useDefaultBillingCurrency();\n const combinedQuotas = useMemo(() => {\n const features = plan.features || [];\n return combineQuotas(features);\n }, [plan.features]);\n\n return (\n <div\n className={`relative border rounded-card p-6 flex flex-col h-full transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] ${\n plan.isActive\n ? 'border-border-seam bg-bg-surface shadow-[var(--shadow-pop)] motion-safe:hover:translate-y-[var(--pop-lift-y)] hover:shadow-[var(--shadow-elevation-2)] [@media(hover:none)]:hover:translate-y-0'\n : 'border-dashed border-text-muted/30 bg-bg-sunken/30 shadow-elevation-1'\n }`}\n >\n {/* Status Badge */}\n {!plan.isActive && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n Inactive\n </span>\n )}\n\n {/* Plan Name */}\n <div className=\"mb-4\">\n <h3 className=\"text-xl font-bold text-text-primary\">{plan.name}</h3>\n <span className=\"text-sm text-text-muted\">{formatPlanDurationLabel(plan.duration)}</span>\n </div>\n\n {/* Price */}\n <div className=\"flex items-baseline gap-1 mb-1\">\n <span className=\"text-3xl font-bold text-text-primary\">\n {(() => {\n const base =\n plan.pricingModel === PricingModel.PER_SEAT && plan.pricePerSeat\n ? { price: plan.pricePerSeat, currency: plan.currency }\n : { price: plan.price, currency: plan.currency };\n const { price, currency } = getDisplayPrice(\n base.price,\n base.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-text-muted\">/ {formatPlanDuration(plan.duration)}</span>\n </div>\n {plan.pricingModel === PricingModel.PER_SEAT && (\n <p className=\"text-xs text-text-muted mb-4\">\n per seat\n {plan.minSeats ? ` · min ${plan.minSeats} seat${plan.minSeats !== 1 ? 's' : ''}` : ''}\n </p>\n )}\n {plan.pricingModel !== PricingModel.PER_SEAT && <div className=\"mb-6\" />}\n\n {/* Features - Combined Quotas */}\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-6\">\n {combinedQuotas.slice(0, 5).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-5 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n <span className=\"text-text-primary\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 5 && (\n <li className=\"text-sm text-text-muted pl-7\">\n +{combinedQuotas.length - 5} more quotas\n </li>\n )}\n </ul>\n )}\n\n {/* Actions - pushed to bottom with mt-auto */}\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n {canSelect && plan.isActive && (\n <button\n type=\"button\"\n onClick={() => onSelect(plan)}\n className=\"w-full 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 Select Plan\n </button>\n )}\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => onView(plan)}\n className=\"flex-1 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 View Details\n </button>\n {canManage && (\n <button\n type=\"button\"\n onClick={() => onToggle(plan)}\n disabled={isToggling}\n className={`px-4 py-2 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n plan.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {plan.isActive ? 'Deactivate' : 'Activate'}\n </button>\n )}\n </div>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Plan Row Component (List View)\n// ============================================================================\n\ninterface PlanRowProps {\n plan: Plan;\n onSelect: (plan: Plan) => void;\n onView: (plan: Plan) => void;\n onToggle: (plan: Plan) => void;\n canSelect: boolean;\n canManage: boolean;\n isToggling: boolean;\n}\n\nconst PlanRow: FC<PlanRowProps> = ({\n plan,\n onSelect,\n onView,\n onToggle,\n canSelect,\n canManage,\n isToggling,\n}) => {\n const displayCurrency = useDefaultBillingCurrency();\n const combinedQuotas = useMemo(() => {\n const features = plan.features || [];\n return combineQuotas(features);\n }, [plan.features]);\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-card shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] ${\n plan.isActive\n ? 'border-border-subtle bg-bg-surface'\n : 'border-dashed border-text-muted/30 bg-bg-sunken/30'\n }`}\n >\n {/* Plan Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-text-primary truncate\">{plan.name}</h3>\n {!plan.isActive && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n Inactive\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-muted\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''} included\n </p>\n </div>\n\n {/* Price */}\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">\n {(() => {\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-text-muted\">per {formatPlanDuration(plan.duration)}</div>\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center gap-2\">\n {canSelect && plan.isActive && (\n <button\n type=\"button\"\n onClick={() => onSelect(plan)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Select\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => onView(plan)}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n View\n </button>\n {canManage && (\n <button\n type=\"button\"\n onClick={() => onToggle(plan)}\n disabled={isToggling}\n className={`px-3 py-1.5 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n plan.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {plan.isActive ? 'Deactivate' : 'Activate'}\n </button>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlansListPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlansListPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plans List Page\n * Displays all available pricing plans\n */\n\nimport { useState, useMemo, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlans, usePlanMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n getPlanFeatureQuantity,\n isServerError,\n} from '../../../shared/utils';\nimport type { Plan, PlanDuration, PlanFeature } from '../../../shared/types';\nimport { PricingModel } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n useDefaultBillingCurrency,\n getDisplayPrice,\n} from '../../../hooks/useDefaultBillingCurrency';\n\ntype ViewMode = 'grid' | 'list';\n\n/**\n * Combined quota info for display\n */\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\n/**\n * Combines duplicate quotas by quota ID and aggregates their values\n */\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n\n for (const feature of features) {\n if (!feature.quota) continue;\n\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n}\n\nexport const PlansListPage: 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 { plans, isLoading, error, refetch } = usePlans();\n const { togglePlan, isToggling } = usePlanMutations();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [showInactive, setShowInactive] = useState(false);\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n\n // Permission check\n if (!permissions.canViewPlans) {\n return (\n <AccessDenied\n message={tr(\n 'billing.plans.noViewPermission',\n \"You don't have permission to view pricing plans.\"\n )}\n />\n );\n }\n\n // Filter plans\n const filteredPlans = plans.filter((plan) => {\n if (!showInactive && !plan.isActive) return false;\n if (billingInterval !== 'all' && plan.duration !== billingInterval) return false;\n return true;\n });\n\n const handleTogglePlan = async (plan: Plan) => {\n if (!permissions.canManagePlans) return;\n try {\n await togglePlan(plan.id, !plan.isActive);\n await refetch();\n } catch (err) {\n console.error('Failed to toggle plan:', err);\n }\n };\n\n const handleSelectPlan = (plan: Plan) => {\n navigateTo(`/checkout/${plan.id}`);\n };\n\n const handleViewPlan = (plan: Plan) => {\n navigateTo(`/plans/${plan.id}`);\n };\n\n // Loading state\n if (isLoading && plans.length === 0) {\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=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} 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-8 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3, 4].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state - show server error component for 500 errors\n if (error) {\n if (isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr(\n 'billing.plans.serverUnavailableMessage',\n 'Unable to load pricing plans. The server might be down or experiencing issues.'\n )}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n // Generic error for non-server errors\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-6 text-status-error-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.failedToLoad', 'Failed to load plans')}\n </h2>\n <p className=\"text-sm text-text-muted\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.plans.title', 'Pricing Plans')}\n </h1>\n <p className=\"text-sm text-text-muted mt-1\">\n {tr('billing.plans.subtitle', 'Choose the plan that best fits your needs')}\n </p>\n </div>\n\n {permissions.canManagePlans && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans/new')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 4v16m8-8H4\"\n />\n </svg>\n Create Plan\n </button>\n )}\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n {/* Billing Interval Toggle */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setBillingInterval('all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === 'all'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n All\n </button>\n <button\n type=\"button\"\n onClick={() => setBillingInterval('monthly' as PlanDuration)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === 'monthly'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n Monthly\n </button>\n <button\n type=\"button\"\n onClick={() => setBillingInterval('yearly' as PlanDuration)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === 'yearly'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n Yearly\n </button>\n </div>\n\n {/* Show Inactive Toggle */}\n {permissions.canManagePlans && (\n <label className=\"flex items-center gap-2 text-sm cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <span className=\"text-text-muted\">\n {tr('billing.plans.showInactive', 'Show inactive plans')}\n </span>\n </label>\n )}\n\n {/* View Mode Toggle */}\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Plans Grid/List */}\n {filteredPlans.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-16 border border-dashed border-border-seam rounded-card bg-bg-surface\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-text-muted\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-text-primary\">\n {tr('billing.plans.noPlansFound', 'No plans found')}\n </h3>\n <p className=\"text-sm text-text-muted mt-1\">\n {showInactive\n ? tr('billing.plans.noPlansFiltered', 'No plans match your current filters.')\n : tr('billing.plans.noActivePlans', 'There are no active plans available.')}\n </p>\n {permissions.canManagePlans && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans/new')}\n className=\"mt-4 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 {tr('billing.plans.createFirstPlan', 'Create your first plan')}\n </button>\n )}\n </div>\n ) : viewMode === 'grid' ? (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {filteredPlans.map((plan) => (\n <PlanCard\n key={plan.id}\n plan={plan}\n onSelect={handleSelectPlan}\n onView={handleViewPlan}\n onToggle={handleTogglePlan}\n canSelect={permissions.canCreateSubscription}\n canManage={permissions.canManagePlans}\n isToggling={isToggling}\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredPlans.map((plan) => (\n <PlanRow\n key={plan.id}\n plan={plan}\n onSelect={handleSelectPlan}\n onView={handleViewPlan}\n onToggle={handleTogglePlan}\n canSelect={permissions.canCreateSubscription}\n canManage={permissions.canManagePlans}\n isToggling={isToggling}\n />\n ))}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Plan Card Component\n// ============================================================================\n\ninterface PlanCardProps {\n plan: Plan;\n onSelect: (plan: Plan) => void;\n onView: (plan: Plan) => void;\n onToggle: (plan: Plan) => void;\n canSelect: boolean;\n canManage: boolean;\n isToggling: boolean;\n}\n\nconst PlanCard: FC<PlanCardProps> = ({\n plan,\n onSelect,\n onView,\n onToggle,\n canSelect,\n canManage,\n isToggling,\n}) => {\n const displayCurrency = useDefaultBillingCurrency();\n const combinedQuotas = useMemo(() => {\n const features = plan.features || [];\n return combineQuotas(features);\n }, [plan.features]);\n\n return (\n <div\n className={`relative border rounded-card p-6 flex flex-col h-full transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] ${\n plan.isActive\n ? 'border-border-seam bg-bg-surface shadow-[var(--shadow-pop)] hover:shadow-[var(--shadow-elevation-2)] [@media(hover:none)]:'\n : 'border-dashed border-text-muted/30 bg-bg-sunken/30 shadow-elevation-1'\n }`}\n >\n {/* Status Badge */}\n {!plan.isActive && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n Inactive\n </span>\n )}\n\n {/* Plan Name */}\n <div className=\"mb-4\">\n <h3 className=\"text-xl font-bold text-text-primary\">{plan.name}</h3>\n <span className=\"text-sm text-text-muted\">{formatPlanDurationLabel(plan.duration)}</span>\n </div>\n\n {/* Price */}\n <div className=\"flex items-baseline gap-1 mb-1\">\n <span className=\"text-3xl font-bold text-text-primary\">\n {(() => {\n const base =\n plan.pricingModel === PricingModel.PER_SEAT && plan.pricePerSeat\n ? { price: plan.pricePerSeat, currency: plan.currency }\n : { price: plan.price, currency: plan.currency };\n const { price, currency } = getDisplayPrice(\n base.price,\n base.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-text-muted\">/ {formatPlanDuration(plan.duration)}</span>\n </div>\n {plan.pricingModel === PricingModel.PER_SEAT && (\n <p className=\"text-xs text-text-muted mb-4\">\n per seat\n {plan.minSeats ? ` · min ${plan.minSeats} seat${plan.minSeats !== 1 ? 's' : ''}` : ''}\n </p>\n )}\n {plan.pricingModel !== PricingModel.PER_SEAT && <div className=\"mb-6\" />}\n\n {/* Features - Combined Quotas */}\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-6\">\n {combinedQuotas.slice(0, 5).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-5 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n <span className=\"text-text-primary\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 5 && (\n <li className=\"text-sm text-text-muted pl-7\">\n +{combinedQuotas.length - 5} more quotas\n </li>\n )}\n </ul>\n )}\n\n {/* Actions - pushed to bottom with mt-auto */}\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n {canSelect && plan.isActive && (\n <button\n type=\"button\"\n onClick={() => onSelect(plan)}\n className=\"w-full 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 Select Plan\n </button>\n )}\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => onView(plan)}\n className=\"flex-1 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 View Details\n </button>\n {canManage && (\n <button\n type=\"button\"\n onClick={() => onToggle(plan)}\n disabled={isToggling}\n className={`px-4 py-2 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n plan.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {plan.isActive ? 'Deactivate' : 'Activate'}\n </button>\n )}\n </div>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Plan Row Component (List View)\n// ============================================================================\n\ninterface PlanRowProps {\n plan: Plan;\n onSelect: (plan: Plan) => void;\n onView: (plan: Plan) => void;\n onToggle: (plan: Plan) => void;\n canSelect: boolean;\n canManage: boolean;\n isToggling: boolean;\n}\n\nconst PlanRow: FC<PlanRowProps> = ({\n plan,\n onSelect,\n onView,\n onToggle,\n canSelect,\n canManage,\n isToggling,\n}) => {\n const displayCurrency = useDefaultBillingCurrency();\n const combinedQuotas = useMemo(() => {\n const features = plan.features || [];\n return combineQuotas(features);\n }, [plan.features]);\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-card shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] ${\n plan.isActive\n ? 'border-border-subtle bg-bg-surface'\n : 'border-dashed border-text-muted/30 bg-bg-sunken/30'\n }`}\n >\n {/* Plan Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-text-primary truncate\">{plan.name}</h3>\n {!plan.isActive && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n Inactive\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-muted\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''} included\n </p>\n </div>\n\n {/* Price */}\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">\n {(() => {\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-text-muted\">per {formatPlanDuration(plan.duration)}</div>\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center gap-2\">\n {canSelect && plan.isActive && (\n <button\n type=\"button\"\n onClick={() => onSelect(plan)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Select\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => onView(plan)}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n View\n </button>\n {canManage && (\n <button\n type=\"button\"\n onClick={() => onToggle(plan)}\n disabled={isToggling}\n className={`px-3 py-1.5 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n plan.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {plan.isActive ? 'Deactivate' : 'Activate'}\n </button>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -65,7 +65,7 @@ var ne = ({ status: e, className: n = "size-4" }) => {
|
|
|
65
65
|
type: "button",
|
|
66
66
|
onClick: o,
|
|
67
67
|
"aria-pressed": a,
|
|
68
|
-
className: `rounded-card border border-border-seam bg-bg-surface bg-accent-wash shadow-elevation-1 p-5 w-full text-left ${o ? "cursor-pointer transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] shadow-[var(--shadow-pop)]
|
|
68
|
+
className: `rounded-card border border-border-seam bg-bg-surface bg-accent-wash shadow-elevation-1 p-5 w-full text-left ${o ? "cursor-pointer transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] shadow-[var(--shadow-pop)] hover:shadow-elevation-2 [@media(hover:none)]: focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]" : ""} ${a ? "ring-1 ring-[var(--color-focus-ring)] border-border-strong" : ""}`,
|
|
69
69
|
children: [/* @__PURE__ */ j("div", {
|
|
70
70
|
className: "flex items-start justify-between gap-3",
|
|
71
71
|
children: [/* @__PURE__ */ A("span", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UsagePage.js","names":[],"sources":["../../../../../src/billing/modules/usage/pages/UsagePage.tsx"],"sourcesContent":["/**\n * Usage Module - Enhanced Usage Page\n * Displays usage dashboard with billing account selector, aggregated quotas by subscription,\n * pooled quotas, and lazy-loaded past subscriptions\n */\n\nimport { useState, useMemo, useDeferredValue, useCallback, type FC } from 'react';\nimport {\n ChevronDown,\n ChevronRight,\n AlertTriangle,\n CheckCircle,\n XCircle,\n AlertCircle,\n Layers,\n Package,\n Clock,\n RefreshCw,\n Info,\n ChevronLeft,\n X,\n Search,\n} from 'lucide-react';\nimport {\n useEnhancedUsagePage,\n useBillingAccountsForUsage,\n useQuotaUsageDetails,\n useWorkspaceQuotaHistory,\n useWorkspaceQuotaOverview,\n type AggregatedQuota,\n type SubscriptionWithAggregatedQuotas,\n type WorkspaceQuotaOverviewItem,\n} from '../hooks/useUsage';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { formatNumber, formatRelativeTime, formatDateTime } from '../../../shared/utils/format';\nimport { statusTokens } from '../../../shared/utils/tokens';\nimport { QuotaStatus } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface QuotaDetailModalProps {\n quota: AggregatedQuota;\n onClose: () => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatQuotaDisplayName(name: string, context?: Record<string, unknown> | null): string {\n if (context?.displayName && typeof context.displayName === 'string') return context.displayName;\n if (context?.label && typeof context.label === 'string') return context.label;\n const parts = name.split('.');\n const meaningful = parts.length > 1 ? parts.slice(1) : parts;\n return meaningful\n .map((p) => p.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase()))\n .join(' ');\n}\n\n// ============================================================================\n// Helper Components\n// ============================================================================\n\nconst StatusIcon: FC<{ status: QuotaStatus; className?: string }> = ({\n status,\n className = 'size-4',\n}) => {\n switch (status) {\n case 'EXHAUSTED':\n return <XCircle className={`${className} ${statusTokens.error.icon}`} />;\n case 'CRITICAL':\n return <AlertTriangle className={`${className} ${statusTokens.error.icon}`} />;\n case 'WARNING':\n return <AlertCircle className={`${className} ${statusTokens.warning.icon}`} />;\n case 'HEALTHY':\n return <CheckCircle className={`${className} ${statusTokens.success.icon}`} />;\n default:\n return <Info className={`${className} ${statusTokens.neutral.icon}`} />;\n }\n};\n\nconst StatusBadge: FC<{ status: QuotaStatus }> = ({ status }) => {\n const colors: Record<QuotaStatus, string> = {\n EXHAUSTED: `${statusTokens.error.bg} ${statusTokens.error.text}`,\n CRITICAL: `${statusTokens.error.bg} ${statusTokens.error.text}`,\n WARNING: `${statusTokens.warning.bg} ${statusTokens.warning.text}`,\n HEALTHY: `${statusTokens.success.bg} ${statusTokens.success.text}`,\n INACTIVE: `${statusTokens.neutral.bg} ${statusTokens.neutral.text}`,\n };\n\n return (\n <span className={`px-2 py-0.5 text-xs font-medium rounded-full ${colors[status]}`}>\n {status}\n </span>\n );\n};\n\nconst ProgressBar: FC<{ percentage: number; status: QuotaStatus }> = ({ percentage, status }) => {\n const clampedPercentage = Math.min(100, Math.max(0, percentage));\n\n const barColors: Record<QuotaStatus, string> = {\n EXHAUSTED: statusTokens.error.dot,\n CRITICAL: statusTokens.error.dot,\n WARNING: statusTokens.warning.dot,\n HEALTHY: statusTokens.success.dot,\n INACTIVE: statusTokens.neutral.dot,\n };\n\n return (\n <div className=\"w-full bg-bg-sunken rounded-full h-2.5 overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all duration-300 ${barColors[status]}`}\n style={{ width: `${clampedPercentage}%` }}\n />\n </div>\n );\n};\n\n// ============================================================================\n// Stat Card Component\n// ============================================================================\n\ntype StatusFilter = 'all' | 'healthy' | 'warning' | 'critical';\n\ninterface StatCardProps {\n label: string;\n value: string | number;\n icon: React.ReactNode;\n color: 'blue' | 'emerald' | 'amber' | 'red' | 'gray';\n isActive?: boolean;\n onClick?: () => void;\n}\n\nconst StatCard: FC<StatCardProps> = ({ label, value, icon, color, isActive, onClick }) => {\n // Map color prop to semantic status tokens\n const colorToStatus = {\n blue: statusTokens.info,\n emerald: statusTokens.success,\n amber: statusTokens.warning,\n red: statusTokens.error,\n gray: statusTokens.neutral,\n };\n\n const tokens = colorToStatus[color];\n\n // Icon sits in a status-tinted tile; text/value use neutral surface tokens.\n const iconColors = {\n blue: statusTokens.info.icon,\n emerald: statusTokens.success.icon,\n amber: statusTokens.warning.icon,\n red: statusTokens.error.icon,\n gray: statusTokens.neutral.icon,\n };\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={isActive}\n className={`rounded-card border border-border-seam bg-bg-surface bg-accent-wash shadow-elevation-1 p-5 w-full text-left ${\n onClick\n ? 'cursor-pointer transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] shadow-[var(--shadow-pop)] motion-safe:hover:translate-y-[var(--pop-lift-y)] hover:shadow-elevation-2 [@media(hover:none)]:hover:translate-y-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n : ''\n } ${isActive ? 'ring-1 ring-[var(--color-focus-ring)] border-border-strong' : ''}`}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <span className=\"text-sm text-text-secondary\">{label}</span>\n <span\n className={`flex size-9 shrink-0 items-center justify-center rounded-lg ${tokens.bg}`}\n >\n <span className={iconColors[color]}>{icon}</span>\n </span>\n </div>\n <p className=\"mt-2 text-2xl font-semibold tabular-nums text-text-primary\">{value}</p>\n </button>\n );\n};\n\n// ============================================================================\n// Quota Table Components\n// ============================================================================\n\ninterface QuotaTableRowProps {\n quota: AggregatedQuota;\n planLabel?: string;\n onClick: () => void;\n}\n\nconst QuotaTableRow: FC<QuotaTableRowProps> = ({ quota, planLabel, onClick }) => {\n const hasLimit = quota.totalLimit != null && quota.totalLimit > 0 && !quota.noLimit;\n\n return (\n <tr\n onClick={onClick}\n className=\"cursor-pointer hover:bg-bg-sunken transition-colors border-b border-border-subtle last:border-b-0\"\n >\n {/* Quota name */}\n <td className=\"px-4 py-3\">\n <span className=\"text-sm font-medium text-text-link hover:underline capitalize\">\n {quota.displayName || quota.name.replace(/_/g, ' ')}\n </span>\n <span className=\"block text-xs font-mono text-text-muted mt-0.5\">{quota.name}</span>\n {quota.description && (\n <span className=\"block text-xs text-text-secondary mt-0.5 max-w-[220px] truncate\">\n {quota.description}\n </span>\n )}\n {quota.quotaCount > 1 && (\n <span className=\"block text-xs text-text-secondary mt-0.5\">\n {quota.quotaCount} assignments\n </span>\n )}\n </td>\n\n {/* Plan / type */}\n {planLabel !== undefined && (\n <td className=\"px-4 py-3 text-sm text-text-secondary whitespace-nowrap\">{planLabel}</td>\n )}\n\n {/* Current usage: value + inline progress bar */}\n <td className=\"px-4 py-3\">\n <div className=\"space-y-1.5\">\n <span className=\"text-sm text-text-primary tabular-nums\">\n {formatNumber(quota.totalUsed)} / \n {hasLimit ? formatNumber(quota.totalLimit) : '∞'}\n {quota.unit ? ` ${quota.unit}` : ''}\n </span>\n <ProgressBar percentage={hasLimit ? quota.usagePercentage : 0} status={quota.status} />\n </div>\n </td>\n\n {/* Limit */}\n <td className=\"px-4 py-3 text-sm text-text-secondary tabular-nums whitespace-nowrap\">\n {hasLimit ? formatNumber(quota.totalLimit) : '∞'}\n </td>\n\n {/* Status */}\n <td className=\"px-4 py-3\">\n <StatusBadge status={quota.status} />\n </td>\n </tr>\n );\n};\n\ninterface QuotaTableProps {\n quotas: AggregatedQuota[];\n /** When provided, renders a \"Plan\" column with this value per row */\n planLabel?: string;\n showPlanColumn?: boolean;\n onQuotaClick: (quota: AggregatedQuota) => void;\n}\n\nconst QuotaTable: FC<QuotaTableProps> = ({\n quotas,\n planLabel,\n showPlanColumn = true,\n onQuotaClick,\n}) => (\n <div className=\"overflow-x-auto rounded-card border border-border-seam bg-bg-surface shadow-elevation-1\">\n <table className=\"w-full text-left\">\n <thead>\n <tr className=\"border-b border-border-subtle bg-bg-sunken\">\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Quotas\n </th>\n {showPlanColumn && (\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Plan\n </th>\n )}\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Current Usage\n </th>\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Limit\n </th>\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Status\n </th>\n </tr>\n </thead>\n <tbody>\n {quotas.map((quota) => (\n <QuotaTableRow\n key={quota.name}\n quota={quota}\n planLabel={showPlanColumn ? planLabel : undefined}\n onClick={() => onQuotaClick(quota)}\n />\n ))}\n </tbody>\n </table>\n </div>\n);\n\n// ============================================================================\n// Subscription Section Component\n// ============================================================================\n\ninterface SubscriptionSectionProps {\n subscription: SubscriptionWithAggregatedQuotas;\n onQuotaClick: (quota: AggregatedQuota) => void;\n defaultExpanded?: boolean;\n}\n\nconst SubscriptionSection: FC<SubscriptionSectionProps> = ({\n subscription,\n onQuotaClick,\n defaultExpanded = true,\n}) => {\n const [isExpanded, setIsExpanded] = useState(defaultExpanded);\n\n const statusColors: Record<string, string> = {\n active: `${statusTokens.success.bg} ${statusTokens.success.text}`,\n trialing: `${statusTokens.info.bg} ${statusTokens.info.text}`,\n past_due: `${statusTokens.warning.bg} ${statusTokens.warning.text}`,\n canceled: `${statusTokens.neutral.bg} ${statusTokens.neutral.text}`,\n expired: `${statusTokens.neutral.bg} ${statusTokens.neutral.text}`,\n upgraded: `${statusTokens.primary.bg} ${statusTokens.primary.text}`,\n };\n\n return (\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n className=\"w-full flex items-center justify-between p-4 hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"flex items-center gap-3\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <Package className=\"size-5 text-text-link\" />\n </span>\n <div className=\"text-left\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium text-text-primary\">Subscription</span>\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded-full capitalize ${statusColors[subscription.status.toLowerCase()] || statusColors.active}`}\n >\n {subscription.status}\n </span>\n </div>\n <p className=\"text-xs text-text-secondary\">\n {subscription.aggregatedQuotas.length} quota type\n {subscription.aggregatedQuotas.length !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n {isExpanded ? (\n <ChevronDown className=\"size-5 text-text-muted\" />\n ) : (\n <ChevronRight className=\"size-5 text-text-muted\" />\n )}\n </button>\n\n {isExpanded && subscription.aggregatedQuotas.length > 0 && (\n <div className=\"border-t border-border-subtle\">\n <QuotaTable\n quotas={subscription.aggregatedQuotas}\n planLabel={subscription.planName || subscription.productId || undefined}\n showPlanColumn={!!(subscription.planName || subscription.productId)}\n onQuotaClick={onQuotaClick}\n />\n </div>\n )}\n\n {isExpanded && subscription.aggregatedQuotas.length === 0 && (\n <div className=\"border-t border-border-subtle p-6 text-center\">\n <p className=\"text-sm text-text-secondary\">No quotas assigned to this subscription</p>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Quota Detail Modal Component\n// ============================================================================\n\nconst QuotaDetailModal: FC<QuotaDetailModalProps> = ({ quota, onClose }) => {\n // Cursor stack: [null] = page 1, [null, c1] = page 2, etc.\n const [cursorStack, setCursorStack] = useState<Array<string | null>>([null]);\n const currentCursor = cursorStack[cursorStack.length - 1] ?? null;\n const currentPage = cursorStack.length;\n const { workspaceId } = useBilling();\n\n // Get usage details for the first quota assignment directly from activity-svc\n const firstAssignmentId = quota.quotaAssignments[0]?.id;\n const { usages, hasNextPage, nextCursor, isLoading } = useQuotaUsageDetails(\n firstAssignmentId,\n 10,\n currentCursor,\n workspaceId\n );\n\n // Snapshot history from quota scraper (activity-svc) — shows live value trend\n const { snapshots, isLoading: isSnapshotsLoading } = useWorkspaceQuotaHistory(\n workspaceId,\n quota.name,\n 24\n );\n\n const handleNext = () => {\n if (nextCursor) setCursorStack((prev) => [...prev, nextCursor]);\n };\n\n const handlePrev = () => {\n if (cursorStack.length > 1) setCursorStack((prev) => prev.slice(0, -1));\n };\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n <div className=\"absolute inset-0 bg-overlay-scrim\" onClick={onClose} />\n <div\n className=\"relative bg-bg-elevated border border-border-seam rounded-card shadow-[var(--shadow-elevation-4)] max-w-2xl w-full mx-4 max-h-[90vh] overflow-auto\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"quota-detail-title\"\n >\n {/* Header */}\n <div className=\"sticky top-0 bg-bg-elevated border-b border-border-subtle p-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <StatusIcon status={quota.status} className=\"size-5\" />\n <div>\n <h2\n id=\"quota-detail-title\"\n className=\"text-lg font-semibold capitalize text-text-primary\"\n >\n {quota.displayName || quota.name.replace(/_/g, ' ')}\n </h2>\n <p className=\"text-xs font-mono text-text-muted\">{quota.name}</p>\n {quota.description && (\n <p className=\"text-sm text-text-secondary mt-0.5\">{quota.description}</p>\n )}\n <p className=\"text-sm text-text-secondary mt-0.5\">\n {quota.quotaCount} quota assignment{quota.quotaCount !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"p-1 rounded-button text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors\"\n aria-label=\"Close quota details\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n\n {/* Summary */}\n <div className=\"p-4 border-b border-border-subtle\">\n <div className=\"mb-4\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Usage</span>\n <StatusBadge status={quota.status} />\n </div>\n <ProgressBar percentage={quota.usagePercentage} status={quota.status} />\n <div className=\"flex items-center justify-between mt-2 text-sm\">\n <span className=\"font-medium text-text-primary tabular-nums\">\n {formatNumber(quota.totalUsed)} /{' '}\n {quota.totalLimit != null && quota.totalLimit > 0\n ? formatNumber(quota.totalLimit)\n : '∞'}\n {quota.unit && ` ${quota.unit}`}\n </span>\n <span className=\"text-text-secondary\">\n {quota.totalLimit != null && quota.totalLimit > 0\n ? `${Math.round(quota.usagePercentage)}% used`\n : 'Unlimited'}\n </span>\n </div>\n </div>\n\n {/* Quota Assignments Breakdown - sorted by expiry date (earliest first) */}\n {quota.quotaCount > 1 && (\n <div className=\"mt-4\">\n <h3 className=\"text-sm font-medium text-text-primary mb-3\">Quota Breakdown</h3>\n <div className=\"border border-border-seam rounded-card overflow-hidden divide-y divide-border-subtle\">\n {[...quota.quotaAssignments]\n .sort((a, b) => {\n // Sort by endtime ascending (earliest expiry first)\n const dateA = a.endtime ? new Date(a.endtime).getTime() : Infinity;\n const dateB = b.endtime ? new Date(b.endtime).getTime() : Infinity;\n return dateA - dateB;\n })\n .map((assignment) => {\n const expiryDate = assignment.endtime ? new Date(assignment.endtime) : null;\n const now = new Date();\n const isExpired = expiryDate && expiryDate < now;\n const daysUntilExpiry = expiryDate\n ? Math.ceil((expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000))\n : null;\n const isExpiringSoon =\n daysUntilExpiry !== null && daysUntilExpiry > 0 && daysUntilExpiry <= 7;\n\n return (\n <div\n key={assignment.id}\n className={`flex items-center justify-between px-3 py-2.5 ${\n isExpired\n ? 'bg-bg-sunken'\n : isExpiringSoon\n ? statusTokens.warning.bg\n : 'bg-bg-surface'\n }`}\n >\n <div className=\"flex items-center gap-2\">\n {isExpiringSoon && !isExpired && (\n <span\n className={`size-2 rounded-full ${statusTokens.warning.dot} animate-pulse`}\n />\n )}\n <div className=\"flex flex-col\">\n <span\n className={`text-sm ${isExpired ? 'text-text-muted' : 'text-text-primary'}`}\n >\n {expiryDate\n ? expiryDate.toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n })\n : 'No expiry'}\n </span>\n <span\n className={`text-xs ${\n isExpired\n ? 'text-status-error-text'\n : isExpiringSoon\n ? statusTokens.warning.text\n : 'text-text-secondary'\n }`}\n >\n {isExpired\n ? 'Expired'\n : daysUntilExpiry !== null\n ? daysUntilExpiry === 0\n ? 'Expires today'\n : daysUntilExpiry === 1\n ? 'Expires tomorrow'\n : `${daysUntilExpiry} days left`\n : 'Never expires'}\n </span>\n </div>\n </div>\n <div className=\"text-right\">\n <span\n className={`text-sm font-semibold tabular-nums ${isExpired ? 'text-text-muted' : 'text-text-primary'}`}\n >\n {formatNumber(assignment.used)}\n <span className=\"text-text-secondary font-normal\">\n {' / '}\n {assignment.limit != null && assignment.limit > 0\n ? formatNumber(assignment.limit)\n : '∞'}\n {assignment.unit && ` ${assignment.unit}`}\n </span>\n </span>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n )}\n </div>\n\n {/* Live Value History (scraper snapshots — at most one per hour) */}\n {(isSnapshotsLoading || snapshots.length > 0) && (\n <div className=\"p-4 border-t border-border-subtle\">\n <h3 className=\"text-sm font-medium text-text-primary mb-3\">Live Value History</h3>\n {isSnapshotsLoading ? (\n <div className=\"space-y-1.5\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-8 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n {snapshots.map((snap) => (\n <div\n key={snap.id}\n className=\"flex items-center justify-between px-3 py-2 bg-bg-sunken rounded-button\"\n >\n <span className=\"text-xs text-text-secondary\">\n {formatDateTime(snap.snapshotAt)}\n </span>\n <span className=\"text-sm font-medium text-text-primary tabular-nums\">\n {formatNumber(snap.value)}\n {quota.unit && (\n <span className=\"text-xs text-text-secondary ml-1\">{quota.unit}</span>\n )}\n {!snap.noLimit && snap.limit != null && (\n <span className=\"text-xs text-text-secondary ml-1\">\n / {formatNumber(snap.limit)}\n </span>\n )}\n </span>\n </div>\n ))}\n </div>\n )}\n </div>\n )}\n\n {/* Usage History */}\n <div className=\"p-4\">\n <h3 className=\"text-sm font-medium text-text-primary mb-3\">Recent Usage</h3>\n\n {quota.isCollectorBased ? (\n <p className=\"text-sm text-text-secondary text-center py-6\">\n Usage is tracked via system metrics, individual event records are not available for\n this quota.\n </p>\n ) : isLoading ? (\n <div className=\"space-y-2\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-12 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n ) : usages.length === 0 ? (\n <p className=\"text-sm text-text-secondary text-center py-6\">No usage records found</p>\n ) : (\n <div className=\"space-y-2\">\n {usages.map((usage) => (\n <div\n key={usage.id}\n className=\"flex items-center justify-between p-3 border border-border-seam rounded-card\"\n >\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n {usage.description || 'Usage recorded'}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatRelativeTime(usage.timestamp)} • {formatDateTime(usage.timestamp)}\n </p>\n </div>\n <span className=\"text-sm font-medium text-text-primary ml-4 tabular-nums\">\n {formatNumber(usage.amount)}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Pagination */}\n {!quota.isCollectorBased && (cursorStack.length > 1 || hasNextPage) && (\n <div className=\"flex items-center justify-center gap-2 mt-4\">\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={cursorStack.length === 1}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button text-text-primary hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n aria-label=\"Previous usage page\"\n >\n <ChevronLeft className=\"size-4\" />\n </button>\n <span className=\"text-sm text-text-secondary tabular-nums\">Page {currentPage}</span>\n <button\n type=\"button\"\n onClick={handleNext}\n disabled={!hasNextPage}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button text-text-primary hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n aria-label=\"Next usage page\"\n >\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"sticky bottom-0 bg-bg-elevated border-t border-border-subtle p-4\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"w-full py-2 px-4 bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Close\n </button>\n </div>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Main Usage Page Component\n// ============================================================================\n\n// ============================================================================\n// Workspace Usage Section — sourced entirely from activity-svc\n// ============================================================================\n\nfunction getWorkspaceQuotaStatus(usagePercentage: number, noLimit: boolean): QuotaStatus {\n if (noLimit) return QuotaStatus.HEALTHY;\n if (usagePercentage >= 100) return QuotaStatus.EXHAUSTED;\n if (usagePercentage >= 90) return QuotaStatus.CRITICAL;\n if (usagePercentage >= 75) return QuotaStatus.WARNING;\n return QuotaStatus.HEALTHY;\n}\n\nconst WorkspaceUsageSection: FC<{ workspaceId: string }> = ({ workspaceId }) => {\n const { overview, isLoading, error, refetch } = useWorkspaceQuotaOverview(workspaceId);\n const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());\n\n // Group rows by quotaName so stacked quotas (multiple subscriptions) collapse into one\n const groups = useMemo(() => {\n const map = new Map<string, WorkspaceQuotaOverviewItem[]>();\n for (const item of overview) {\n const existing = map.get(item.assignment.quotaName) ?? [];\n existing.push(item);\n map.set(item.assignment.quotaName, existing);\n }\n return Array.from(map.entries()).map(([quotaName, items]) => {\n const noLimit = items.every((i) => i.assignment.noLimit);\n const totalLimit = noLimit\n ? null\n : items.reduce((sum, i) => sum + (i.assignment.limit ?? 0), 0);\n const totalUsed = items.reduce((sum, i) => sum + i.effectiveUsed, 0);\n const totalRemaining = totalLimit != null ? totalLimit - totalUsed : null;\n const usagePercentage = totalLimit ? (totalUsed / totalLimit) * 100 : 0;\n const status = getWorkspaceQuotaStatus(usagePercentage, noLimit);\n const hasLive = items.some((i) => i.liveStatus !== null);\n return {\n quotaName,\n items,\n noLimit,\n totalLimit,\n totalUsed,\n totalRemaining,\n usagePercentage,\n status,\n hasLive,\n hasMultiple: items.length > 1,\n };\n });\n }, [overview]);\n\n const toggleGroup = useCallback((quotaName: string) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev);\n if (next.has(quotaName)) {\n next.delete(quotaName);\n } else {\n next.add(quotaName);\n }\n return next;\n });\n }, []);\n\n if (isLoading && overview.length === 0) {\n return (\n <div className=\"space-y-3\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-16 bg-bg-sunken animate-pulse rounded-card\" />\n ))}\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"text-center py-8 space-y-2\">\n <AlertTriangle className=\"size-8 text-status-error-text mx-auto\" />\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n Retry\n </button>\n </div>\n );\n }\n\n if (overview.length === 0) {\n return (\n <div className=\"text-center py-12 space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-2\">\n <Layers className=\"size-6 text-text-link\" />\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">No workspace quotas assigned</h3>\n <p className=\"text-sm text-text-secondary max-w-sm mx-auto\">\n Quotas are assigned to this workspace after a successful purchase. If you have an active\n subscription, contact support.\n </p>\n </div>\n );\n }\n\n // Summary stats count grouped quota names, not raw rows\n const healthy = groups.filter((g) => g.status === QuotaStatus.HEALTHY).length;\n const warning = groups.filter((g) => g.status === QuotaStatus.WARNING).length;\n const critical = groups.filter(\n (g) => g.status === QuotaStatus.CRITICAL || g.status === QuotaStatus.EXHAUSTED\n ).length;\n\n return (\n <div className=\"space-y-4\">\n {/* Summary row */}\n <div className=\"grid grid-cols-3 gap-3\">\n <div className=\"rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4\">\n <div className=\"flex items-center gap-1.5 mb-1\">\n <span\n className={`flex size-7 items-center justify-center rounded-lg ${statusTokens.success.bg}`}\n >\n <CheckCircle className={`size-4 ${statusTokens.success.icon}`} />\n </span>\n <span className=\"text-xs text-text-secondary\">Healthy</span>\n </div>\n <p className=\"text-xl font-semibold tabular-nums text-text-primary\">{healthy}</p>\n </div>\n <div className=\"rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4\">\n <div className=\"flex items-center gap-1.5 mb-1\">\n <span\n className={`flex size-7 items-center justify-center rounded-lg ${statusTokens.warning.bg}`}\n >\n <AlertCircle className={`size-4 ${statusTokens.warning.icon}`} />\n </span>\n <span className=\"text-xs text-text-secondary\">Warning</span>\n </div>\n <p className=\"text-xl font-semibold tabular-nums text-text-primary\">{warning}</p>\n </div>\n <div className=\"rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4\">\n <div className=\"flex items-center gap-1.5 mb-1\">\n <span\n className={`flex size-7 items-center justify-center rounded-lg ${statusTokens.error.bg}`}\n >\n <XCircle className={`size-4 ${statusTokens.error.icon}`} />\n </span>\n <span className=\"text-xs text-text-secondary\">Critical / Exhausted</span>\n </div>\n <p className=\"text-xl font-semibold tabular-nums text-text-primary\">{critical}</p>\n </div>\n </div>\n\n {/* Quota table */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle bg-bg-sunken\">\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs\">\n Quota\n </th>\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs hidden sm:table-cell\">\n Sources\n </th>\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs\">\n Usage\n </th>\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs hidden sm:table-cell\">\n Remaining\n </th>\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs\">\n Status\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {groups.map((group) => {\n const isExpanded = expandedGroups.has(group.quotaName);\n return (\n <>\n {/* Collapsed summary row — one per unique quotaName */}\n <tr\n key={group.quotaName}\n className={`transition-colors ${group.hasMultiple ? 'cursor-pointer hover:bg-bg-sunken' : 'hover:bg-bg-sunken'}`}\n onClick={group.hasMultiple ? () => toggleGroup(group.quotaName) : undefined}\n >\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {group.hasMultiple && (\n <ChevronRight\n className={`size-4 text-text-muted shrink-0 transition-transform duration-150 ${isExpanded ? 'rotate-90' : ''}`}\n />\n )}\n <div>\n <div className=\"font-medium text-text-primary\">\n {formatQuotaDisplayName(group.quotaName)}\n </div>\n <div className=\"text-xs font-mono text-text-muted mt-0.5\">\n {group.quotaName}\n </div>\n {group.hasLive && (\n <div className=\"text-xs text-text-secondary mt-0.5\">\n Live · updated ~60s\n </div>\n )}\n </div>\n </div>\n </td>\n <td className=\"px-4 py-3 hidden sm:table-cell\">\n {group.hasMultiple ? (\n <span className=\"text-xs bg-[var(--color-accent-soft)] text-text-link px-1.5 py-0.5 rounded font-medium\">\n {group.items.length} subscriptions\n </span>\n ) : (\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-xs text-text-secondary bg-bg-sunken px-1.5 py-0.5 rounded\">\n {group.items[0]?.assignment.productId}\n </span>\n <span className=\"text-xs text-text-secondary capitalize\">\n {group.items[0]?.assignment.quotaMode}\n </span>\n </div>\n )}\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"space-y-1.5\">\n <div className=\"text-text-primary tabular-nums\">\n {formatNumber(group.totalUsed)}\n {!group.noLimit && group.totalLimit != null && (\n <span className=\"text-text-secondary\">\n {' '}\n / {formatNumber(group.totalLimit)}\n </span>\n )}\n </div>\n {!group.noLimit && (\n <ProgressBar percentage={group.usagePercentage} status={group.status} />\n )}\n </div>\n </td>\n <td className=\"px-4 py-3 hidden sm:table-cell text-text-primary tabular-nums\">\n {group.noLimit ? (\n <span className=\"text-text-secondary\">Unlimited</span>\n ) : (\n formatNumber(group.totalRemaining ?? 0)\n )}\n </td>\n <td className=\"px-4 py-3\">\n <StatusBadge status={group.status} />\n </td>\n </tr>\n\n {/* Expanded detail rows — one per subscription/assignment */}\n {isExpanded &&\n group.items.map((item) => {\n const subStatus = getWorkspaceQuotaStatus(\n item.usagePercentage,\n item.assignment.noLimit\n );\n return (\n <tr\n key={item.assignment.id}\n className=\"bg-bg-sunken/50 border-t border-dashed border-border-subtle\"\n >\n <td className=\"pl-12 pr-4 py-2.5\">\n <div className=\"text-xs text-text-secondary space-y-0.5\">\n <div\n className=\"font-mono truncate max-w-[200px]\"\n title={item.assignment.subscriptionId ?? undefined}\n >\n {item.assignment.subscriptionId ?? '—'}\n </div>\n <div className=\"flex items-center gap-1.5 flex-wrap\">\n <span className=\"bg-bg-sunken px-1.5 py-0.5 rounded\">\n {item.assignment.productId}\n </span>\n <span className=\"capitalize\">{item.assignment.quotaMode}</span>\n </div>\n </div>\n </td>\n <td className=\"px-4 py-2.5 hidden sm:table-cell\" />\n <td className=\"px-4 py-2.5 text-xs text-text-secondary tabular-nums\">\n {formatNumber(item.effectiveUsed)}\n {!item.assignment.noLimit && item.assignment.limit != null && (\n <> / {formatNumber(item.assignment.limit)}</>\n )}\n </td>\n <td className=\"px-4 py-2.5 hidden sm:table-cell text-xs text-text-secondary tabular-nums\">\n {item.assignment.noLimit\n ? 'Unlimited'\n : formatNumber(item.remaining ?? 0)}\n </td>\n <td className=\"px-4 py-2.5\">\n <StatusBadge status={subStatus} />\n </td>\n </tr>\n );\n })}\n </>\n );\n })}\n </tbody>\n </table>\n </div>\n\n <p className=\"text-xs text-text-secondary\">\n Workspace quotas are sourced from the activity service. Usage is tracked cumulatively across\n all active sources for each quota.\n {overview.some((o) => o.liveStatus) && ' Live values are updated every ~60 seconds.'}\n </p>\n </div>\n );\n};\n\n// ============================================================================\n// Main Usage Page\n// ============================================================================\n\nexport const UsagePage: 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 permissions = useBillingPermissions();\n\n // Helper to get billing account ID from URL (for navigation from Overview page)\n const getInitialBillingAccountId = (): string | undefined => {\n if (typeof window === 'undefined') return undefined;\n const urlParams = new URLSearchParams(window.location.search);\n return urlParams.get('billingAccountId') || undefined;\n };\n\n // State - initialize with URL param if present\n const [selectedBillingAccountId, setSelectedBillingAccountId] = useState<string | undefined>(\n getInitialBillingAccountId\n );\n const [selectedQuota, setSelectedQuota] = useState<AggregatedQuota | null>(null);\n\n // Search and filter state\n const [searchQuery, setSearchQuery] = useState('');\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');\n\n // View mode: 'active' | 'past' | 'all'\n type ViewMode = 'active' | 'past' | 'all';\n const [viewMode, setViewMode] = useState<ViewMode>('active');\n\n // Debounce search query to avoid too many API calls\n const deferredSearchQuery = useDeferredValue(searchQuery);\n\n // Fetch billing accounts\n const { billingAccounts, isLoading: isLoadingAccounts } = useBillingAccountsForUsage();\n\n // Prefer the default account, fall back to first\n const defaultBillingAccountId = (billingAccounts.find((a) => a.isDefault) ?? billingAccounts[0])\n ?.id;\n // ?? so that an explicit empty string (cleared URL param) still falls back to default\n const effectiveBillingAccountId = selectedBillingAccountId ?? defaultBillingAccountId;\n\n const { workspaceId, navigate } = useBilling();\n\n // Top-level view: 'global' = billing service quotas, 'workspace' = activity-svc quotas\n const [usageView, setUsageView] = useState<'global' | 'workspace'>('workspace');\n\n // Fetch usage data - passes searchQuery and viewMode to backend for server-side filtering\n const {\n dashboard,\n pooledQuotas,\n activeSubscriptions,\n pastSubscriptions,\n isLoading,\n isPastSubscriptionsLoading,\n error,\n refetch,\n // Pagination for past subscriptions\n loadMorePastSubscriptions,\n hasMorePastSubscriptions,\n isLoadingMorePast,\n pastSubscriptionsCount,\n } = useEnhancedUsagePage({\n billingAccountId: effectiveBillingAccountId,\n workspaceId,\n searchQuery: deferredSearchQuery || undefined,\n viewMode, // 'active' | 'past' | 'all'\n });\n\n // Filter quotas by status (search is now handled by backend)\n const filterQuotaByStatus = useCallback(\n (quota: AggregatedQuota): boolean => {\n if (statusFilter === 'all') return true;\n if (statusFilter === 'healthy') return quota.status === 'HEALTHY';\n if (statusFilter === 'warning') return quota.status === 'WARNING';\n if (statusFilter === 'critical')\n return quota.status === 'CRITICAL' || quota.status === 'EXHAUSTED';\n return true;\n },\n [statusFilter]\n );\n\n // Filtered data - only status filter is client-side, search is backend\n const filteredPooledQuotas = useMemo(() => {\n return pooledQuotas.filter(filterQuotaByStatus);\n }, [pooledQuotas, filterQuotaByStatus]);\n\n const filteredActiveSubscriptions = useMemo(() => {\n if (statusFilter === 'all') {\n return activeSubscriptions;\n }\n return activeSubscriptions\n .map((sub) => ({\n ...sub,\n aggregatedQuotas: sub.aggregatedQuotas.filter(filterQuotaByStatus),\n }))\n .filter((sub) => sub.aggregatedQuotas.length > 0);\n }, [activeSubscriptions, statusFilter, filterQuotaByStatus]);\n\n const filteredPastSubscriptions = useMemo(() => {\n if (statusFilter === 'all') {\n return pastSubscriptions;\n }\n return pastSubscriptions\n .map((sub) => ({\n ...sub,\n aggregatedQuotas: sub.aggregatedQuotas.filter(filterQuotaByStatus),\n }))\n .filter((sub) => sub.aggregatedQuotas.length > 0);\n }, [pastSubscriptions, statusFilter, filterQuotaByStatus]);\n\n // Subscriptions to display based on view mode\n const displayedSubscriptions = useMemo(() => {\n if (viewMode === 'active') {\n return { active: filteredActiveSubscriptions, past: [] };\n }\n if (viewMode === 'past') {\n return { active: [], past: filteredPastSubscriptions };\n }\n // 'all' - show both\n return { active: filteredActiveSubscriptions, past: filteredPastSubscriptions };\n }, [viewMode, filteredActiveSubscriptions, filteredPastSubscriptions]);\n\n const hasActiveFilters = searchQuery !== '' || statusFilter !== 'all';\n const isSearching = deferredSearchQuery !== searchQuery; // Show loading indicator while debouncing\n\n const clearFilters = () => {\n setSearchQuery('');\n setStatusFilter('all');\n };\n\n // Toggle status filter (click same to deselect)\n const toggleStatusFilter = (filter: StatusFilter) => {\n setStatusFilter((prev) => (prev === filter ? 'all' : filter));\n };\n\n // Permission check\n if (!permissions.canViewUsage) {\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-[var(--color-accent-soft)] flex items-center justify-center\">\n <AlertTriangle 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 view usage data.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if ((isLoadingAccounts || isLoading) && !dashboard) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n {[1, 2, 3, 4].map((i) => (\n <div key={i} className=\"h-24 bg-bg-sunken animate-pulse rounded-card\" />\n ))}\n </div>\n <div className=\"h-64 bg-bg-sunken animate-pulse rounded-card\" />\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <AlertTriangle className=\"size-6 text-status-error-text\" />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Failed to load usage data</h2>\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n const selectedAccount = billingAccounts.find((a) => a.id === effectiveBillingAccountId);\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.usage.title', 'Usage')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n Monitor your resource consumption and quota usage\n </p>\n </div>\n\n <div className=\"flex items-center gap-3\">\n {/* Global / Workspace view toggle */}\n <div className=\"flex items-center bg-bg-sunken rounded-lg p-1 border border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => setUsageView('workspace')}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-button transition-all duration-200 ${\n usageView === 'workspace'\n ? 'bg-bg-surface text-text-primary shadow-elevation-1 font-medium'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n <Layers className=\"size-3.5\" />\n Workspace\n </button>\n <button\n type=\"button\"\n onClick={() => setUsageView('global')}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-button transition-all duration-200 ${\n usageView === 'global'\n ? 'bg-bg-surface text-text-primary shadow-elevation-1 font-medium'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n <Package className=\"size-3.5\" />\n Global\n </button>\n </div>\n\n {/* Billing Account Selector — only shown for global view */}\n {usageView === 'global' && billingAccounts.length > 1 && (\n <select\n value={effectiveBillingAccountId || ''}\n onChange={(e) => setSelectedBillingAccountId(e.target.value)}\n className=\"px-3 py-2 text-sm border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] w-full sm:min-w-[200px]\"\n >\n {billingAccounts.map((account) => (\n <option key={account.id} value={account.id}>\n {account.name}\n </option>\n ))}\n </select>\n )}\n\n {/* Refresh Button */}\n <button\n type=\"button\"\n onClick={() => (usageView === 'workspace' ? undefined : refetch())}\n className=\"p-2 border border-border-subtle rounded-button text-text-secondary hover:text-text-primary hover:bg-bg-sunken hover:border-border-strong transition-all duration-200\"\n aria-label=\"Refresh\"\n title=\"Refresh\"\n >\n <RefreshCw className=\"size-4\" />\n </button>\n </div>\n </div>\n\n {/* Workspace Usage Section */}\n {usageView === 'workspace' && workspaceId && (\n <WorkspaceUsageSection workspaceId={workspaceId} />\n )}\n\n {usageView === 'workspace' && !workspaceId && (\n <div className=\"text-center py-12 text-text-secondary text-sm\">\n No workspace context available.\n </div>\n )}\n\n {/* Global Usage Section — Search and Filters */}\n {usageView === 'global' && (\n <>\n {/* Search and Filters */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center\">\n {/* Search Input */}\n <div className=\"relative flex-1 max-w-md\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-muted\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder={`Search ${viewMode === 'past' ? 'past' : viewMode === 'all' ? 'all' : 'active'} quotas...`}\n className=\"w-full pl-9 pr-3 py-2 text-sm border border-border-subtle rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {(searchQuery || isSearching) && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery('')}\n aria-label=\"Clear search\"\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-bg-sunken\"\n >\n {isSearching ? (\n <RefreshCw className=\"size-3 text-text-muted animate-spin\" />\n ) : (\n <X className=\"size-3 text-text-muted\" />\n )}\n </button>\n )}\n </div>\n\n {/* View Mode Tabs */}\n <div className=\"flex items-center bg-bg-sunken rounded-lg p-1 border border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => setViewMode('active')}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-button transition-all duration-200 ${\n viewMode === 'active'\n ? 'bg-bg-surface text-text-primary shadow-elevation-1 font-medium'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n <Package className=\"size-3.5\" />\n Active\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('past')}\n disabled={isPastSubscriptionsLoading && viewMode !== 'past'}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-button transition-all duration-200 ${\n viewMode === 'past'\n ? 'bg-bg-surface text-text-primary shadow-elevation-1 font-medium'\n : 'text-text-secondary hover:text-text-primary'\n } ${isPastSubscriptionsLoading && viewMode !== 'past' ? 'opacity-70' : ''}`}\n >\n {isPastSubscriptionsLoading && viewMode === 'past' ? (\n <RefreshCw className=\"size-3.5 animate-spin\" />\n ) : (\n <Clock className=\"size-3.5\" />\n )}\n Past\n </button>\n </div>\n\n {/* Active Filters Indicator */}\n {hasActiveFilters && (\n <button\n type=\"button\"\n onClick={clearFilters}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary border border-border-subtle rounded-button hover:bg-bg-sunken transition-colors\"\n >\n <X className=\"size-3\" />\n Clear filters\n </button>\n )}\n </div>\n\n {/* Billing Account Info */}\n {selectedAccount && (\n <div className=\"text-sm text-text-secondary\">\n Viewing usage for{' '}\n <span className=\"font-medium text-text-primary\">{selectedAccount.name}</span>\n </div>\n )}\n\n {/* Overview Stats - Clickable to filter */}\n {dashboard && (\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <StatCard\n label=\"Total Quotas\"\n value={dashboard.totalQuotas}\n icon={<Layers className=\"size-5\" />}\n color=\"blue\"\n isActive={statusFilter === 'all'}\n onClick={() => setStatusFilter('all')}\n />\n <StatCard\n label=\"Healthy\"\n value={dashboard.healthyQuotas}\n icon={<CheckCircle className=\"size-5\" />}\n color=\"emerald\"\n isActive={statusFilter === 'healthy'}\n onClick={() => toggleStatusFilter('healthy')}\n />\n <StatCard\n label=\"Warning\"\n value={dashboard.warningQuotas}\n icon={<AlertCircle className=\"size-5\" />}\n color=\"amber\"\n isActive={statusFilter === 'warning'}\n onClick={() => toggleStatusFilter('warning')}\n />\n <StatCard\n label=\"Critical / Exhausted\"\n value={dashboard.criticalQuotas + dashboard.exhaustedQuotas}\n icon={<AlertTriangle className=\"size-5\" />}\n color=\"red\"\n isActive={statusFilter === 'critical'}\n onClick={() => toggleStatusFilter('critical')}\n />\n </div>\n )}\n\n {/* Request More Quota Banner — shown when any quotas are warning or critical/exhausted */}\n {dashboard &&\n (dashboard.warningQuotas > 0 ||\n dashboard.criticalQuotas > 0 ||\n dashboard.exhaustedQuotas > 0) && (\n <div\n className={`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between p-4 rounded-lg border ${\n dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0\n ? `${statusTokens.error.bg} ${statusTokens.error.border}`\n : `${statusTokens.warning.bg} ${statusTokens.warning.border}`\n }`}\n >\n <div className=\"flex items-center gap-3\">\n {dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0 ? (\n <XCircle className={`size-5 shrink-0 ${statusTokens.error.icon}`} />\n ) : (\n <AlertCircle className={`size-5 shrink-0 ${statusTokens.warning.icon}`} />\n )}\n <div>\n <p\n className={`text-sm font-medium ${\n dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0\n ? statusTokens.error.text\n : statusTokens.warning.text\n }`}\n >\n {dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0\n ? `${dashboard.criticalQuotas + dashboard.exhaustedQuotas} quota${dashboard.criticalQuotas + dashboard.exhaustedQuotas !== 1 ? 's' : ''} exhausted or critical`\n : `${dashboard.warningQuotas} quota${dashboard.warningQuotas !== 1 ? 's' : ''} approaching the limit`}\n </p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n Contact support to request a quota increase for your plan.\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate('/support/tickets')}\n className={`shrink-0 px-4 py-2 text-sm font-medium rounded-button border transition-colors duration-200 ${\n dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0\n ? 'bg-action-danger-bg text-action-danger-text border-action-danger-bg hover:bg-action-danger-bgHover'\n : 'border-status-warning-border text-status-warning-text hover:bg-status-warning-bg-subtle'\n }`}\n >\n Request more quota\n </button>\n </div>\n )}\n\n {/* Filter Results Info */}\n {(hasActiveFilters || viewMode !== 'active') && (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary flex-wrap\">\n <span>\n Showing{' '}\n {displayedSubscriptions.active.reduce(\n (acc, s) => acc + s.aggregatedQuotas.length,\n 0\n ) +\n displayedSubscriptions.past.reduce(\n (acc, s) => acc + s.aggregatedQuotas.length,\n 0\n ) +\n (viewMode !== 'past' ? filteredPooledQuotas.length : 0)}{' '}\n quotas\n </span>\n <span className=\"px-2 py-0.5 bg-[var(--color-accent-soft)] text-text-link rounded text-xs capitalize\">\n {viewMode === 'active' ? 'Active' : viewMode === 'past' ? 'Past' : 'All'}{' '}\n subscriptions\n </span>\n {statusFilter !== 'all' && (\n <span className=\"px-2 py-0.5 bg-bg-sunken text-text-secondary rounded text-xs capitalize\">\n {statusFilter === 'critical' ? 'Critical / Exhausted' : statusFilter}\n </span>\n )}\n {searchQuery && (\n <span className=\"px-2 py-0.5 bg-bg-sunken text-text-secondary rounded text-xs\">\n “{searchQuery}”\n </span>\n )}\n </div>\n )}\n\n {/* Active Subscriptions Section */}\n {displayedSubscriptions.active.length > 0 && (\n <section>\n <h2 className=\"text-lg font-semibold text-text-primary mb-4 flex items-center gap-2\">\n <Package className=\"size-5 text-text-link\" />\n {viewMode === 'all' ? 'Active Subscription Quotas' : 'Subscription Quotas'}\n </h2>\n <div className=\"space-y-4\">\n {displayedSubscriptions.active.map((subscription) => (\n <SubscriptionSection\n key={subscription.id}\n subscription={subscription}\n onQuotaClick={setSelectedQuota}\n defaultExpanded={true}\n />\n ))}\n </div>\n </section>\n )}\n\n {/* Past Subscriptions Section (when viewMode is 'past' or 'all') */}\n {displayedSubscriptions.past.length > 0 && (\n <section>\n <h2 className=\"text-lg font-semibold text-text-primary mb-4 flex items-center gap-2\">\n <Clock className=\"size-5 text-text-muted\" />\n Past Subscription Quotas\n <span className=\"text-xs font-normal text-text-secondary\">\n (Canceled / Expired / Upgraded)\n </span>\n {pastSubscriptionsCount > 0 && (\n <span className=\"text-xs font-normal px-2 py-0.5 bg-bg-sunken text-text-secondary rounded-full tabular-nums\">\n {displayedSubscriptions.past.length}\n {hasMorePastSubscriptions ? '+' : ''} of {pastSubscriptionsCount}\n </span>\n )}\n </h2>\n <div className=\"space-y-4\">\n {displayedSubscriptions.past.map((subscription) => (\n <SubscriptionSection\n key={subscription.id}\n subscription={subscription}\n onQuotaClick={setSelectedQuota}\n defaultExpanded={viewMode === 'past'}\n />\n ))}\n\n {/* Load More Button */}\n {hasMorePastSubscriptions && (\n <div className=\"flex justify-center pt-2\">\n <button\n type=\"button\"\n onClick={loadMorePastSubscriptions}\n disabled={isLoadingMorePast}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-text-secondary hover:text-text-primary border border-border-subtle rounded-button hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n {isLoadingMorePast ? (\n <>\n <RefreshCw className=\"size-4 animate-spin\" />\n Loading…\n </>\n ) : (\n <>\n <ChevronDown className=\"size-4\" />\n Load more past subscriptions\n </>\n )}\n </button>\n </div>\n )}\n </div>\n </section>\n )}\n\n {/* Pooled Quotas Section - Only show for active/all views (pooled quotas are account-level) */}\n {viewMode !== 'past' && filteredPooledQuotas.length > 0 && (\n <section>\n <h2 className=\"text-lg font-semibold text-text-primary mb-4 flex items-center gap-2\">\n <Layers className={`size-5 ${statusTokens.info.icon}`} />\n Pooled Quotas\n <span className=\"text-xs font-normal text-text-secondary\">\n (Shared across all products)\n </span>\n </h2>\n <QuotaTable\n quotas={filteredPooledQuotas}\n showPlanColumn={false}\n onQuotaClick={setSelectedQuota}\n />\n </section>\n )}\n\n {/* Empty State - No quotas at all (when viewing active) */}\n {viewMode === 'active' &&\n activeSubscriptions.length === 0 &&\n pooledQuotas.length === 0 &&\n !hasActiveFilters && (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border-seam rounded-card\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <Layers className=\"size-6 text-text-link\" />\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">\n No active quotas found\n </h3>\n <p className=\"text-sm text-text-secondary mt-1 text-center max-w-sm\">\n Quotas will appear here once you have active subscriptions or pooled allocations.\n </p>\n </div>\n )}\n\n {/* Empty State - No past subscriptions */}\n {viewMode === 'past' &&\n pastSubscriptions.length === 0 &&\n !hasActiveFilters &&\n !isPastSubscriptionsLoading && (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border-seam rounded-card\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <Clock className=\"size-6 text-text-link\" />\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">No past subscriptions</h3>\n <p className=\"text-sm text-text-secondary mt-1 text-center max-w-sm\">\n You don't have any canceled, expired, or upgraded subscriptions yet.\n </p>\n <button\n type=\"button\"\n onClick={() => setViewMode('active')}\n className=\"mt-4 px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n View active subscriptions\n </button>\n </div>\n )}\n\n {/* Loading State for Past Subscriptions */}\n {viewMode === 'past' && isPastSubscriptionsLoading && (\n <div className=\"space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-24 bg-bg-sunken animate-pulse rounded-card\" />\n ))}\n </div>\n )}\n\n {/* Empty State - No results from search/filter */}\n {hasActiveFilters &&\n displayedSubscriptions.active.length === 0 &&\n displayedSubscriptions.past.length === 0 &&\n (viewMode === 'past' || filteredPooledQuotas.length === 0) && (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border-seam rounded-card\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n {statusFilter !== 'all' && !searchQuery ? (\n <StatusIcon\n status={\n (statusFilter === 'healthy'\n ? 'HEALTHY'\n : statusFilter === 'warning'\n ? 'WARNING'\n : 'CRITICAL') as QuotaStatus\n }\n className=\"size-6\"\n />\n ) : (\n <Search className=\"size-6 text-text-link\" />\n )}\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">\n {statusFilter !== 'all' && !searchQuery\n ? `No ${statusFilter === 'critical' ? 'critical or exhausted' : statusFilter} quotas`\n : 'No matching quotas'}\n </h3>\n <p className=\"text-sm text-text-secondary mt-1 text-center max-w-sm\">\n {statusFilter !== 'all' && !searchQuery ? (\n <>\n All your quotas are in a different status.\n {dashboard && (\n <span className=\"block mt-1\">\n You have {dashboard.healthyQuotas} healthy, {dashboard.warningQuotas}{' '}\n warning, and {dashboard.criticalQuotas + dashboard.exhaustedQuotas}{' '}\n critical quotas.\n </span>\n )}\n </>\n ) : (\n <>\n No quotas match your current search or filter criteria in{' '}\n {viewMode === 'past' ? 'past' : viewMode === 'all' ? 'any' : 'active'}{' '}\n subscriptions.\n {viewMode === 'active' && !statusFilter.includes('all') && (\n <span className=\"block mt-1\">\n Try switching to “Past” or “All” to search across\n more subscriptions.\n </span>\n )}\n </>\n )}\n </p>\n <div className=\"flex gap-2 mt-4\">\n <button\n type=\"button\"\n onClick={clearFilters}\n className=\"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 {statusFilter !== 'all' ? 'Show all quotas' : 'Clear filters'}\n </button>\n {viewMode === 'active' && searchQuery && (\n <button\n type=\"button\"\n onClick={() => setViewMode('all')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Search all\n </button>\n )}\n </div>\n </div>\n )}\n </>\n )}\n\n {/* Quota Detail Modal */}\n {selectedQuota && (\n <QuotaDetailModal quota={selectedQuota} onClose={() => setSelectedQuota(null)} />\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UsagePage.js","names":[],"sources":["../../../../../src/billing/modules/usage/pages/UsagePage.tsx"],"sourcesContent":["/**\n * Usage Module - Enhanced Usage Page\n * Displays usage dashboard with billing account selector, aggregated quotas by subscription,\n * pooled quotas, and lazy-loaded past subscriptions\n */\n\nimport { useState, useMemo, useDeferredValue, useCallback, type FC } from 'react';\nimport {\n ChevronDown,\n ChevronRight,\n AlertTriangle,\n CheckCircle,\n XCircle,\n AlertCircle,\n Layers,\n Package,\n Clock,\n RefreshCw,\n Info,\n ChevronLeft,\n X,\n Search,\n} from 'lucide-react';\nimport {\n useEnhancedUsagePage,\n useBillingAccountsForUsage,\n useQuotaUsageDetails,\n useWorkspaceQuotaHistory,\n useWorkspaceQuotaOverview,\n type AggregatedQuota,\n type SubscriptionWithAggregatedQuotas,\n type WorkspaceQuotaOverviewItem,\n} from '../hooks/useUsage';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { formatNumber, formatRelativeTime, formatDateTime } from '../../../shared/utils/format';\nimport { statusTokens } from '../../../shared/utils/tokens';\nimport { QuotaStatus } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface QuotaDetailModalProps {\n quota: AggregatedQuota;\n onClose: () => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatQuotaDisplayName(name: string, context?: Record<string, unknown> | null): string {\n if (context?.displayName && typeof context.displayName === 'string') return context.displayName;\n if (context?.label && typeof context.label === 'string') return context.label;\n const parts = name.split('.');\n const meaningful = parts.length > 1 ? parts.slice(1) : parts;\n return meaningful\n .map((p) => p.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase()))\n .join(' ');\n}\n\n// ============================================================================\n// Helper Components\n// ============================================================================\n\nconst StatusIcon: FC<{ status: QuotaStatus; className?: string }> = ({\n status,\n className = 'size-4',\n}) => {\n switch (status) {\n case 'EXHAUSTED':\n return <XCircle className={`${className} ${statusTokens.error.icon}`} />;\n case 'CRITICAL':\n return <AlertTriangle className={`${className} ${statusTokens.error.icon}`} />;\n case 'WARNING':\n return <AlertCircle className={`${className} ${statusTokens.warning.icon}`} />;\n case 'HEALTHY':\n return <CheckCircle className={`${className} ${statusTokens.success.icon}`} />;\n default:\n return <Info className={`${className} ${statusTokens.neutral.icon}`} />;\n }\n};\n\nconst StatusBadge: FC<{ status: QuotaStatus }> = ({ status }) => {\n const colors: Record<QuotaStatus, string> = {\n EXHAUSTED: `${statusTokens.error.bg} ${statusTokens.error.text}`,\n CRITICAL: `${statusTokens.error.bg} ${statusTokens.error.text}`,\n WARNING: `${statusTokens.warning.bg} ${statusTokens.warning.text}`,\n HEALTHY: `${statusTokens.success.bg} ${statusTokens.success.text}`,\n INACTIVE: `${statusTokens.neutral.bg} ${statusTokens.neutral.text}`,\n };\n\n return (\n <span className={`px-2 py-0.5 text-xs font-medium rounded-full ${colors[status]}`}>\n {status}\n </span>\n );\n};\n\nconst ProgressBar: FC<{ percentage: number; status: QuotaStatus }> = ({ percentage, status }) => {\n const clampedPercentage = Math.min(100, Math.max(0, percentage));\n\n const barColors: Record<QuotaStatus, string> = {\n EXHAUSTED: statusTokens.error.dot,\n CRITICAL: statusTokens.error.dot,\n WARNING: statusTokens.warning.dot,\n HEALTHY: statusTokens.success.dot,\n INACTIVE: statusTokens.neutral.dot,\n };\n\n return (\n <div className=\"w-full bg-bg-sunken rounded-full h-2.5 overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all duration-300 ${barColors[status]}`}\n style={{ width: `${clampedPercentage}%` }}\n />\n </div>\n );\n};\n\n// ============================================================================\n// Stat Card Component\n// ============================================================================\n\ntype StatusFilter = 'all' | 'healthy' | 'warning' | 'critical';\n\ninterface StatCardProps {\n label: string;\n value: string | number;\n icon: React.ReactNode;\n color: 'blue' | 'emerald' | 'amber' | 'red' | 'gray';\n isActive?: boolean;\n onClick?: () => void;\n}\n\nconst StatCard: FC<StatCardProps> = ({ label, value, icon, color, isActive, onClick }) => {\n // Map color prop to semantic status tokens\n const colorToStatus = {\n blue: statusTokens.info,\n emerald: statusTokens.success,\n amber: statusTokens.warning,\n red: statusTokens.error,\n gray: statusTokens.neutral,\n };\n\n const tokens = colorToStatus[color];\n\n // Icon sits in a status-tinted tile; text/value use neutral surface tokens.\n const iconColors = {\n blue: statusTokens.info.icon,\n emerald: statusTokens.success.icon,\n amber: statusTokens.warning.icon,\n red: statusTokens.error.icon,\n gray: statusTokens.neutral.icon,\n };\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={isActive}\n className={`rounded-card border border-border-seam bg-bg-surface bg-accent-wash shadow-elevation-1 p-5 w-full text-left ${\n onClick\n ? 'cursor-pointer transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] shadow-[var(--shadow-pop)] hover:shadow-elevation-2 [@media(hover:none)]: focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n : ''\n } ${isActive ? 'ring-1 ring-[var(--color-focus-ring)] border-border-strong' : ''}`}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <span className=\"text-sm text-text-secondary\">{label}</span>\n <span\n className={`flex size-9 shrink-0 items-center justify-center rounded-lg ${tokens.bg}`}\n >\n <span className={iconColors[color]}>{icon}</span>\n </span>\n </div>\n <p className=\"mt-2 text-2xl font-semibold tabular-nums text-text-primary\">{value}</p>\n </button>\n );\n};\n\n// ============================================================================\n// Quota Table Components\n// ============================================================================\n\ninterface QuotaTableRowProps {\n quota: AggregatedQuota;\n planLabel?: string;\n onClick: () => void;\n}\n\nconst QuotaTableRow: FC<QuotaTableRowProps> = ({ quota, planLabel, onClick }) => {\n const hasLimit = quota.totalLimit != null && quota.totalLimit > 0 && !quota.noLimit;\n\n return (\n <tr\n onClick={onClick}\n className=\"cursor-pointer hover:bg-bg-sunken transition-colors border-b border-border-subtle last:border-b-0\"\n >\n {/* Quota name */}\n <td className=\"px-4 py-3\">\n <span className=\"text-sm font-medium text-text-link hover:underline capitalize\">\n {quota.displayName || quota.name.replace(/_/g, ' ')}\n </span>\n <span className=\"block text-xs font-mono text-text-muted mt-0.5\">{quota.name}</span>\n {quota.description && (\n <span className=\"block text-xs text-text-secondary mt-0.5 max-w-[220px] truncate\">\n {quota.description}\n </span>\n )}\n {quota.quotaCount > 1 && (\n <span className=\"block text-xs text-text-secondary mt-0.5\">\n {quota.quotaCount} assignments\n </span>\n )}\n </td>\n\n {/* Plan / type */}\n {planLabel !== undefined && (\n <td className=\"px-4 py-3 text-sm text-text-secondary whitespace-nowrap\">{planLabel}</td>\n )}\n\n {/* Current usage: value + inline progress bar */}\n <td className=\"px-4 py-3\">\n <div className=\"space-y-1.5\">\n <span className=\"text-sm text-text-primary tabular-nums\">\n {formatNumber(quota.totalUsed)} / \n {hasLimit ? formatNumber(quota.totalLimit) : '∞'}\n {quota.unit ? ` ${quota.unit}` : ''}\n </span>\n <ProgressBar percentage={hasLimit ? quota.usagePercentage : 0} status={quota.status} />\n </div>\n </td>\n\n {/* Limit */}\n <td className=\"px-4 py-3 text-sm text-text-secondary tabular-nums whitespace-nowrap\">\n {hasLimit ? formatNumber(quota.totalLimit) : '∞'}\n </td>\n\n {/* Status */}\n <td className=\"px-4 py-3\">\n <StatusBadge status={quota.status} />\n </td>\n </tr>\n );\n};\n\ninterface QuotaTableProps {\n quotas: AggregatedQuota[];\n /** When provided, renders a \"Plan\" column with this value per row */\n planLabel?: string;\n showPlanColumn?: boolean;\n onQuotaClick: (quota: AggregatedQuota) => void;\n}\n\nconst QuotaTable: FC<QuotaTableProps> = ({\n quotas,\n planLabel,\n showPlanColumn = true,\n onQuotaClick,\n}) => (\n <div className=\"overflow-x-auto rounded-card border border-border-seam bg-bg-surface shadow-elevation-1\">\n <table className=\"w-full text-left\">\n <thead>\n <tr className=\"border-b border-border-subtle bg-bg-sunken\">\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Quotas\n </th>\n {showPlanColumn && (\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Plan\n </th>\n )}\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Current Usage\n </th>\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Limit\n </th>\n <th className=\"px-4 py-2.5 text-xs font-semibold text-text-muted uppercase tracking-wider\">\n Status\n </th>\n </tr>\n </thead>\n <tbody>\n {quotas.map((quota) => (\n <QuotaTableRow\n key={quota.name}\n quota={quota}\n planLabel={showPlanColumn ? planLabel : undefined}\n onClick={() => onQuotaClick(quota)}\n />\n ))}\n </tbody>\n </table>\n </div>\n);\n\n// ============================================================================\n// Subscription Section Component\n// ============================================================================\n\ninterface SubscriptionSectionProps {\n subscription: SubscriptionWithAggregatedQuotas;\n onQuotaClick: (quota: AggregatedQuota) => void;\n defaultExpanded?: boolean;\n}\n\nconst SubscriptionSection: FC<SubscriptionSectionProps> = ({\n subscription,\n onQuotaClick,\n defaultExpanded = true,\n}) => {\n const [isExpanded, setIsExpanded] = useState(defaultExpanded);\n\n const statusColors: Record<string, string> = {\n active: `${statusTokens.success.bg} ${statusTokens.success.text}`,\n trialing: `${statusTokens.info.bg} ${statusTokens.info.text}`,\n past_due: `${statusTokens.warning.bg} ${statusTokens.warning.text}`,\n canceled: `${statusTokens.neutral.bg} ${statusTokens.neutral.text}`,\n expired: `${statusTokens.neutral.bg} ${statusTokens.neutral.text}`,\n upgraded: `${statusTokens.primary.bg} ${statusTokens.primary.text}`,\n };\n\n return (\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n className=\"w-full flex items-center justify-between p-4 hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"flex items-center gap-3\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <Package className=\"size-5 text-text-link\" />\n </span>\n <div className=\"text-left\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium text-text-primary\">Subscription</span>\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded-full capitalize ${statusColors[subscription.status.toLowerCase()] || statusColors.active}`}\n >\n {subscription.status}\n </span>\n </div>\n <p className=\"text-xs text-text-secondary\">\n {subscription.aggregatedQuotas.length} quota type\n {subscription.aggregatedQuotas.length !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n {isExpanded ? (\n <ChevronDown className=\"size-5 text-text-muted\" />\n ) : (\n <ChevronRight className=\"size-5 text-text-muted\" />\n )}\n </button>\n\n {isExpanded && subscription.aggregatedQuotas.length > 0 && (\n <div className=\"border-t border-border-subtle\">\n <QuotaTable\n quotas={subscription.aggregatedQuotas}\n planLabel={subscription.planName || subscription.productId || undefined}\n showPlanColumn={!!(subscription.planName || subscription.productId)}\n onQuotaClick={onQuotaClick}\n />\n </div>\n )}\n\n {isExpanded && subscription.aggregatedQuotas.length === 0 && (\n <div className=\"border-t border-border-subtle p-6 text-center\">\n <p className=\"text-sm text-text-secondary\">No quotas assigned to this subscription</p>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Quota Detail Modal Component\n// ============================================================================\n\nconst QuotaDetailModal: FC<QuotaDetailModalProps> = ({ quota, onClose }) => {\n // Cursor stack: [null] = page 1, [null, c1] = page 2, etc.\n const [cursorStack, setCursorStack] = useState<Array<string | null>>([null]);\n const currentCursor = cursorStack[cursorStack.length - 1] ?? null;\n const currentPage = cursorStack.length;\n const { workspaceId } = useBilling();\n\n // Get usage details for the first quota assignment directly from activity-svc\n const firstAssignmentId = quota.quotaAssignments[0]?.id;\n const { usages, hasNextPage, nextCursor, isLoading } = useQuotaUsageDetails(\n firstAssignmentId,\n 10,\n currentCursor,\n workspaceId\n );\n\n // Snapshot history from quota scraper (activity-svc) — shows live value trend\n const { snapshots, isLoading: isSnapshotsLoading } = useWorkspaceQuotaHistory(\n workspaceId,\n quota.name,\n 24\n );\n\n const handleNext = () => {\n if (nextCursor) setCursorStack((prev) => [...prev, nextCursor]);\n };\n\n const handlePrev = () => {\n if (cursorStack.length > 1) setCursorStack((prev) => prev.slice(0, -1));\n };\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n <div className=\"absolute inset-0 bg-overlay-scrim\" onClick={onClose} />\n <div\n className=\"relative bg-bg-elevated border border-border-seam rounded-card shadow-[var(--shadow-elevation-4)] max-w-2xl w-full mx-4 max-h-[90vh] overflow-auto\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"quota-detail-title\"\n >\n {/* Header */}\n <div className=\"sticky top-0 bg-bg-elevated border-b border-border-subtle p-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <StatusIcon status={quota.status} className=\"size-5\" />\n <div>\n <h2\n id=\"quota-detail-title\"\n className=\"text-lg font-semibold capitalize text-text-primary\"\n >\n {quota.displayName || quota.name.replace(/_/g, ' ')}\n </h2>\n <p className=\"text-xs font-mono text-text-muted\">{quota.name}</p>\n {quota.description && (\n <p className=\"text-sm text-text-secondary mt-0.5\">{quota.description}</p>\n )}\n <p className=\"text-sm text-text-secondary mt-0.5\">\n {quota.quotaCount} quota assignment{quota.quotaCount !== 1 ? 's' : ''}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"p-1 rounded-button text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors\"\n aria-label=\"Close quota details\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n\n {/* Summary */}\n <div className=\"p-4 border-b border-border-subtle\">\n <div className=\"mb-4\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Usage</span>\n <StatusBadge status={quota.status} />\n </div>\n <ProgressBar percentage={quota.usagePercentage} status={quota.status} />\n <div className=\"flex items-center justify-between mt-2 text-sm\">\n <span className=\"font-medium text-text-primary tabular-nums\">\n {formatNumber(quota.totalUsed)} /{' '}\n {quota.totalLimit != null && quota.totalLimit > 0\n ? formatNumber(quota.totalLimit)\n : '∞'}\n {quota.unit && ` ${quota.unit}`}\n </span>\n <span className=\"text-text-secondary\">\n {quota.totalLimit != null && quota.totalLimit > 0\n ? `${Math.round(quota.usagePercentage)}% used`\n : 'Unlimited'}\n </span>\n </div>\n </div>\n\n {/* Quota Assignments Breakdown - sorted by expiry date (earliest first) */}\n {quota.quotaCount > 1 && (\n <div className=\"mt-4\">\n <h3 className=\"text-sm font-medium text-text-primary mb-3\">Quota Breakdown</h3>\n <div className=\"border border-border-seam rounded-card overflow-hidden divide-y divide-border-subtle\">\n {[...quota.quotaAssignments]\n .sort((a, b) => {\n // Sort by endtime ascending (earliest expiry first)\n const dateA = a.endtime ? new Date(a.endtime).getTime() : Infinity;\n const dateB = b.endtime ? new Date(b.endtime).getTime() : Infinity;\n return dateA - dateB;\n })\n .map((assignment) => {\n const expiryDate = assignment.endtime ? new Date(assignment.endtime) : null;\n const now = new Date();\n const isExpired = expiryDate && expiryDate < now;\n const daysUntilExpiry = expiryDate\n ? Math.ceil((expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000))\n : null;\n const isExpiringSoon =\n daysUntilExpiry !== null && daysUntilExpiry > 0 && daysUntilExpiry <= 7;\n\n return (\n <div\n key={assignment.id}\n className={`flex items-center justify-between px-3 py-2.5 ${\n isExpired\n ? 'bg-bg-sunken'\n : isExpiringSoon\n ? statusTokens.warning.bg\n : 'bg-bg-surface'\n }`}\n >\n <div className=\"flex items-center gap-2\">\n {isExpiringSoon && !isExpired && (\n <span\n className={`size-2 rounded-full ${statusTokens.warning.dot} animate-pulse`}\n />\n )}\n <div className=\"flex flex-col\">\n <span\n className={`text-sm ${isExpired ? 'text-text-muted' : 'text-text-primary'}`}\n >\n {expiryDate\n ? expiryDate.toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n })\n : 'No expiry'}\n </span>\n <span\n className={`text-xs ${\n isExpired\n ? 'text-status-error-text'\n : isExpiringSoon\n ? statusTokens.warning.text\n : 'text-text-secondary'\n }`}\n >\n {isExpired\n ? 'Expired'\n : daysUntilExpiry !== null\n ? daysUntilExpiry === 0\n ? 'Expires today'\n : daysUntilExpiry === 1\n ? 'Expires tomorrow'\n : `${daysUntilExpiry} days left`\n : 'Never expires'}\n </span>\n </div>\n </div>\n <div className=\"text-right\">\n <span\n className={`text-sm font-semibold tabular-nums ${isExpired ? 'text-text-muted' : 'text-text-primary'}`}\n >\n {formatNumber(assignment.used)}\n <span className=\"text-text-secondary font-normal\">\n {' / '}\n {assignment.limit != null && assignment.limit > 0\n ? formatNumber(assignment.limit)\n : '∞'}\n {assignment.unit && ` ${assignment.unit}`}\n </span>\n </span>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n )}\n </div>\n\n {/* Live Value History (scraper snapshots — at most one per hour) */}\n {(isSnapshotsLoading || snapshots.length > 0) && (\n <div className=\"p-4 border-t border-border-subtle\">\n <h3 className=\"text-sm font-medium text-text-primary mb-3\">Live Value History</h3>\n {isSnapshotsLoading ? (\n <div className=\"space-y-1.5\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-8 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n {snapshots.map((snap) => (\n <div\n key={snap.id}\n className=\"flex items-center justify-between px-3 py-2 bg-bg-sunken rounded-button\"\n >\n <span className=\"text-xs text-text-secondary\">\n {formatDateTime(snap.snapshotAt)}\n </span>\n <span className=\"text-sm font-medium text-text-primary tabular-nums\">\n {formatNumber(snap.value)}\n {quota.unit && (\n <span className=\"text-xs text-text-secondary ml-1\">{quota.unit}</span>\n )}\n {!snap.noLimit && snap.limit != null && (\n <span className=\"text-xs text-text-secondary ml-1\">\n / {formatNumber(snap.limit)}\n </span>\n )}\n </span>\n </div>\n ))}\n </div>\n )}\n </div>\n )}\n\n {/* Usage History */}\n <div className=\"p-4\">\n <h3 className=\"text-sm font-medium text-text-primary mb-3\">Recent Usage</h3>\n\n {quota.isCollectorBased ? (\n <p className=\"text-sm text-text-secondary text-center py-6\">\n Usage is tracked via system metrics, individual event records are not available for\n this quota.\n </p>\n ) : isLoading ? (\n <div className=\"space-y-2\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-12 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n ) : usages.length === 0 ? (\n <p className=\"text-sm text-text-secondary text-center py-6\">No usage records found</p>\n ) : (\n <div className=\"space-y-2\">\n {usages.map((usage) => (\n <div\n key={usage.id}\n className=\"flex items-center justify-between p-3 border border-border-seam rounded-card\"\n >\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n {usage.description || 'Usage recorded'}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatRelativeTime(usage.timestamp)} • {formatDateTime(usage.timestamp)}\n </p>\n </div>\n <span className=\"text-sm font-medium text-text-primary ml-4 tabular-nums\">\n {formatNumber(usage.amount)}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Pagination */}\n {!quota.isCollectorBased && (cursorStack.length > 1 || hasNextPage) && (\n <div className=\"flex items-center justify-center gap-2 mt-4\">\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={cursorStack.length === 1}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button text-text-primary hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n aria-label=\"Previous usage page\"\n >\n <ChevronLeft className=\"size-4\" />\n </button>\n <span className=\"text-sm text-text-secondary tabular-nums\">Page {currentPage}</span>\n <button\n type=\"button\"\n onClick={handleNext}\n disabled={!hasNextPage}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button text-text-primary hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n aria-label=\"Next usage page\"\n >\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"sticky bottom-0 bg-bg-elevated border-t border-border-subtle p-4\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"w-full py-2 px-4 bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Close\n </button>\n </div>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Main Usage Page Component\n// ============================================================================\n\n// ============================================================================\n// Workspace Usage Section — sourced entirely from activity-svc\n// ============================================================================\n\nfunction getWorkspaceQuotaStatus(usagePercentage: number, noLimit: boolean): QuotaStatus {\n if (noLimit) return QuotaStatus.HEALTHY;\n if (usagePercentage >= 100) return QuotaStatus.EXHAUSTED;\n if (usagePercentage >= 90) return QuotaStatus.CRITICAL;\n if (usagePercentage >= 75) return QuotaStatus.WARNING;\n return QuotaStatus.HEALTHY;\n}\n\nconst WorkspaceUsageSection: FC<{ workspaceId: string }> = ({ workspaceId }) => {\n const { overview, isLoading, error, refetch } = useWorkspaceQuotaOverview(workspaceId);\n const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());\n\n // Group rows by quotaName so stacked quotas (multiple subscriptions) collapse into one\n const groups = useMemo(() => {\n const map = new Map<string, WorkspaceQuotaOverviewItem[]>();\n for (const item of overview) {\n const existing = map.get(item.assignment.quotaName) ?? [];\n existing.push(item);\n map.set(item.assignment.quotaName, existing);\n }\n return Array.from(map.entries()).map(([quotaName, items]) => {\n const noLimit = items.every((i) => i.assignment.noLimit);\n const totalLimit = noLimit\n ? null\n : items.reduce((sum, i) => sum + (i.assignment.limit ?? 0), 0);\n const totalUsed = items.reduce((sum, i) => sum + i.effectiveUsed, 0);\n const totalRemaining = totalLimit != null ? totalLimit - totalUsed : null;\n const usagePercentage = totalLimit ? (totalUsed / totalLimit) * 100 : 0;\n const status = getWorkspaceQuotaStatus(usagePercentage, noLimit);\n const hasLive = items.some((i) => i.liveStatus !== null);\n return {\n quotaName,\n items,\n noLimit,\n totalLimit,\n totalUsed,\n totalRemaining,\n usagePercentage,\n status,\n hasLive,\n hasMultiple: items.length > 1,\n };\n });\n }, [overview]);\n\n const toggleGroup = useCallback((quotaName: string) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev);\n if (next.has(quotaName)) {\n next.delete(quotaName);\n } else {\n next.add(quotaName);\n }\n return next;\n });\n }, []);\n\n if (isLoading && overview.length === 0) {\n return (\n <div className=\"space-y-3\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-16 bg-bg-sunken animate-pulse rounded-card\" />\n ))}\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"text-center py-8 space-y-2\">\n <AlertTriangle className=\"size-8 text-status-error-text mx-auto\" />\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-3 py-1.5 text-sm border border-border-subtle rounded-button text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n Retry\n </button>\n </div>\n );\n }\n\n if (overview.length === 0) {\n return (\n <div className=\"text-center py-12 space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-2\">\n <Layers className=\"size-6 text-text-link\" />\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">No workspace quotas assigned</h3>\n <p className=\"text-sm text-text-secondary max-w-sm mx-auto\">\n Quotas are assigned to this workspace after a successful purchase. If you have an active\n subscription, contact support.\n </p>\n </div>\n );\n }\n\n // Summary stats count grouped quota names, not raw rows\n const healthy = groups.filter((g) => g.status === QuotaStatus.HEALTHY).length;\n const warning = groups.filter((g) => g.status === QuotaStatus.WARNING).length;\n const critical = groups.filter(\n (g) => g.status === QuotaStatus.CRITICAL || g.status === QuotaStatus.EXHAUSTED\n ).length;\n\n return (\n <div className=\"space-y-4\">\n {/* Summary row */}\n <div className=\"grid grid-cols-3 gap-3\">\n <div className=\"rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4\">\n <div className=\"flex items-center gap-1.5 mb-1\">\n <span\n className={`flex size-7 items-center justify-center rounded-lg ${statusTokens.success.bg}`}\n >\n <CheckCircle className={`size-4 ${statusTokens.success.icon}`} />\n </span>\n <span className=\"text-xs text-text-secondary\">Healthy</span>\n </div>\n <p className=\"text-xl font-semibold tabular-nums text-text-primary\">{healthy}</p>\n </div>\n <div className=\"rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4\">\n <div className=\"flex items-center gap-1.5 mb-1\">\n <span\n className={`flex size-7 items-center justify-center rounded-lg ${statusTokens.warning.bg}`}\n >\n <AlertCircle className={`size-4 ${statusTokens.warning.icon}`} />\n </span>\n <span className=\"text-xs text-text-secondary\">Warning</span>\n </div>\n <p className=\"text-xl font-semibold tabular-nums text-text-primary\">{warning}</p>\n </div>\n <div className=\"rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4\">\n <div className=\"flex items-center gap-1.5 mb-1\">\n <span\n className={`flex size-7 items-center justify-center rounded-lg ${statusTokens.error.bg}`}\n >\n <XCircle className={`size-4 ${statusTokens.error.icon}`} />\n </span>\n <span className=\"text-xs text-text-secondary\">Critical / Exhausted</span>\n </div>\n <p className=\"text-xl font-semibold tabular-nums text-text-primary\">{critical}</p>\n </div>\n </div>\n\n {/* Quota table */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle bg-bg-sunken\">\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs\">\n Quota\n </th>\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs hidden sm:table-cell\">\n Sources\n </th>\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs\">\n Usage\n </th>\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs hidden sm:table-cell\">\n Remaining\n </th>\n <th className=\"text-left px-4 py-3 font-medium text-text-muted uppercase tracking-wide text-xs\">\n Status\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {groups.map((group) => {\n const isExpanded = expandedGroups.has(group.quotaName);\n return (\n <>\n {/* Collapsed summary row — one per unique quotaName */}\n <tr\n key={group.quotaName}\n className={`transition-colors ${group.hasMultiple ? 'cursor-pointer hover:bg-bg-sunken' : 'hover:bg-bg-sunken'}`}\n onClick={group.hasMultiple ? () => toggleGroup(group.quotaName) : undefined}\n >\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {group.hasMultiple && (\n <ChevronRight\n className={`size-4 text-text-muted shrink-0 transition-transform duration-150 ${isExpanded ? 'rotate-90' : ''}`}\n />\n )}\n <div>\n <div className=\"font-medium text-text-primary\">\n {formatQuotaDisplayName(group.quotaName)}\n </div>\n <div className=\"text-xs font-mono text-text-muted mt-0.5\">\n {group.quotaName}\n </div>\n {group.hasLive && (\n <div className=\"text-xs text-text-secondary mt-0.5\">\n Live · updated ~60s\n </div>\n )}\n </div>\n </div>\n </td>\n <td className=\"px-4 py-3 hidden sm:table-cell\">\n {group.hasMultiple ? (\n <span className=\"text-xs bg-[var(--color-accent-soft)] text-text-link px-1.5 py-0.5 rounded font-medium\">\n {group.items.length} subscriptions\n </span>\n ) : (\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-xs text-text-secondary bg-bg-sunken px-1.5 py-0.5 rounded\">\n {group.items[0]?.assignment.productId}\n </span>\n <span className=\"text-xs text-text-secondary capitalize\">\n {group.items[0]?.assignment.quotaMode}\n </span>\n </div>\n )}\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"space-y-1.5\">\n <div className=\"text-text-primary tabular-nums\">\n {formatNumber(group.totalUsed)}\n {!group.noLimit && group.totalLimit != null && (\n <span className=\"text-text-secondary\">\n {' '}\n / {formatNumber(group.totalLimit)}\n </span>\n )}\n </div>\n {!group.noLimit && (\n <ProgressBar percentage={group.usagePercentage} status={group.status} />\n )}\n </div>\n </td>\n <td className=\"px-4 py-3 hidden sm:table-cell text-text-primary tabular-nums\">\n {group.noLimit ? (\n <span className=\"text-text-secondary\">Unlimited</span>\n ) : (\n formatNumber(group.totalRemaining ?? 0)\n )}\n </td>\n <td className=\"px-4 py-3\">\n <StatusBadge status={group.status} />\n </td>\n </tr>\n\n {/* Expanded detail rows — one per subscription/assignment */}\n {isExpanded &&\n group.items.map((item) => {\n const subStatus = getWorkspaceQuotaStatus(\n item.usagePercentage,\n item.assignment.noLimit\n );\n return (\n <tr\n key={item.assignment.id}\n className=\"bg-bg-sunken/50 border-t border-dashed border-border-subtle\"\n >\n <td className=\"pl-12 pr-4 py-2.5\">\n <div className=\"text-xs text-text-secondary space-y-0.5\">\n <div\n className=\"font-mono truncate max-w-[200px]\"\n title={item.assignment.subscriptionId ?? undefined}\n >\n {item.assignment.subscriptionId ?? '—'}\n </div>\n <div className=\"flex items-center gap-1.5 flex-wrap\">\n <span className=\"bg-bg-sunken px-1.5 py-0.5 rounded\">\n {item.assignment.productId}\n </span>\n <span className=\"capitalize\">{item.assignment.quotaMode}</span>\n </div>\n </div>\n </td>\n <td className=\"px-4 py-2.5 hidden sm:table-cell\" />\n <td className=\"px-4 py-2.5 text-xs text-text-secondary tabular-nums\">\n {formatNumber(item.effectiveUsed)}\n {!item.assignment.noLimit && item.assignment.limit != null && (\n <> / {formatNumber(item.assignment.limit)}</>\n )}\n </td>\n <td className=\"px-4 py-2.5 hidden sm:table-cell text-xs text-text-secondary tabular-nums\">\n {item.assignment.noLimit\n ? 'Unlimited'\n : formatNumber(item.remaining ?? 0)}\n </td>\n <td className=\"px-4 py-2.5\">\n <StatusBadge status={subStatus} />\n </td>\n </tr>\n );\n })}\n </>\n );\n })}\n </tbody>\n </table>\n </div>\n\n <p className=\"text-xs text-text-secondary\">\n Workspace quotas are sourced from the activity service. Usage is tracked cumulatively across\n all active sources for each quota.\n {overview.some((o) => o.liveStatus) && ' Live values are updated every ~60 seconds.'}\n </p>\n </div>\n );\n};\n\n// ============================================================================\n// Main Usage Page\n// ============================================================================\n\nexport const UsagePage: 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 permissions = useBillingPermissions();\n\n // Helper to get billing account ID from URL (for navigation from Overview page)\n const getInitialBillingAccountId = (): string | undefined => {\n if (typeof window === 'undefined') return undefined;\n const urlParams = new URLSearchParams(window.location.search);\n return urlParams.get('billingAccountId') || undefined;\n };\n\n // State - initialize with URL param if present\n const [selectedBillingAccountId, setSelectedBillingAccountId] = useState<string | undefined>(\n getInitialBillingAccountId\n );\n const [selectedQuota, setSelectedQuota] = useState<AggregatedQuota | null>(null);\n\n // Search and filter state\n const [searchQuery, setSearchQuery] = useState('');\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');\n\n // View mode: 'active' | 'past' | 'all'\n type ViewMode = 'active' | 'past' | 'all';\n const [viewMode, setViewMode] = useState<ViewMode>('active');\n\n // Debounce search query to avoid too many API calls\n const deferredSearchQuery = useDeferredValue(searchQuery);\n\n // Fetch billing accounts\n const { billingAccounts, isLoading: isLoadingAccounts } = useBillingAccountsForUsage();\n\n // Prefer the default account, fall back to first\n const defaultBillingAccountId = (billingAccounts.find((a) => a.isDefault) ?? billingAccounts[0])\n ?.id;\n // ?? so that an explicit empty string (cleared URL param) still falls back to default\n const effectiveBillingAccountId = selectedBillingAccountId ?? defaultBillingAccountId;\n\n const { workspaceId, navigate } = useBilling();\n\n // Top-level view: 'global' = billing service quotas, 'workspace' = activity-svc quotas\n const [usageView, setUsageView] = useState<'global' | 'workspace'>('workspace');\n\n // Fetch usage data - passes searchQuery and viewMode to backend for server-side filtering\n const {\n dashboard,\n pooledQuotas,\n activeSubscriptions,\n pastSubscriptions,\n isLoading,\n isPastSubscriptionsLoading,\n error,\n refetch,\n // Pagination for past subscriptions\n loadMorePastSubscriptions,\n hasMorePastSubscriptions,\n isLoadingMorePast,\n pastSubscriptionsCount,\n } = useEnhancedUsagePage({\n billingAccountId: effectiveBillingAccountId,\n workspaceId,\n searchQuery: deferredSearchQuery || undefined,\n viewMode, // 'active' | 'past' | 'all'\n });\n\n // Filter quotas by status (search is now handled by backend)\n const filterQuotaByStatus = useCallback(\n (quota: AggregatedQuota): boolean => {\n if (statusFilter === 'all') return true;\n if (statusFilter === 'healthy') return quota.status === 'HEALTHY';\n if (statusFilter === 'warning') return quota.status === 'WARNING';\n if (statusFilter === 'critical')\n return quota.status === 'CRITICAL' || quota.status === 'EXHAUSTED';\n return true;\n },\n [statusFilter]\n );\n\n // Filtered data - only status filter is client-side, search is backend\n const filteredPooledQuotas = useMemo(() => {\n return pooledQuotas.filter(filterQuotaByStatus);\n }, [pooledQuotas, filterQuotaByStatus]);\n\n const filteredActiveSubscriptions = useMemo(() => {\n if (statusFilter === 'all') {\n return activeSubscriptions;\n }\n return activeSubscriptions\n .map((sub) => ({\n ...sub,\n aggregatedQuotas: sub.aggregatedQuotas.filter(filterQuotaByStatus),\n }))\n .filter((sub) => sub.aggregatedQuotas.length > 0);\n }, [activeSubscriptions, statusFilter, filterQuotaByStatus]);\n\n const filteredPastSubscriptions = useMemo(() => {\n if (statusFilter === 'all') {\n return pastSubscriptions;\n }\n return pastSubscriptions\n .map((sub) => ({\n ...sub,\n aggregatedQuotas: sub.aggregatedQuotas.filter(filterQuotaByStatus),\n }))\n .filter((sub) => sub.aggregatedQuotas.length > 0);\n }, [pastSubscriptions, statusFilter, filterQuotaByStatus]);\n\n // Subscriptions to display based on view mode\n const displayedSubscriptions = useMemo(() => {\n if (viewMode === 'active') {\n return { active: filteredActiveSubscriptions, past: [] };\n }\n if (viewMode === 'past') {\n return { active: [], past: filteredPastSubscriptions };\n }\n // 'all' - show both\n return { active: filteredActiveSubscriptions, past: filteredPastSubscriptions };\n }, [viewMode, filteredActiveSubscriptions, filteredPastSubscriptions]);\n\n const hasActiveFilters = searchQuery !== '' || statusFilter !== 'all';\n const isSearching = deferredSearchQuery !== searchQuery; // Show loading indicator while debouncing\n\n const clearFilters = () => {\n setSearchQuery('');\n setStatusFilter('all');\n };\n\n // Toggle status filter (click same to deselect)\n const toggleStatusFilter = (filter: StatusFilter) => {\n setStatusFilter((prev) => (prev === filter ? 'all' : filter));\n };\n\n // Permission check\n if (!permissions.canViewUsage) {\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-[var(--color-accent-soft)] flex items-center justify-center\">\n <AlertTriangle 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 view usage data.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if ((isLoadingAccounts || isLoading) && !dashboard) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n {[1, 2, 3, 4].map((i) => (\n <div key={i} className=\"h-24 bg-bg-sunken animate-pulse rounded-card\" />\n ))}\n </div>\n <div className=\"h-64 bg-bg-sunken animate-pulse rounded-card\" />\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <AlertTriangle className=\"size-6 text-status-error-text\" />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Failed to load usage data</h2>\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n const selectedAccount = billingAccounts.find((a) => a.id === effectiveBillingAccountId);\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {tr('billing.usage.title', 'Usage')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n Monitor your resource consumption and quota usage\n </p>\n </div>\n\n <div className=\"flex items-center gap-3\">\n {/* Global / Workspace view toggle */}\n <div className=\"flex items-center bg-bg-sunken rounded-lg p-1 border border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => setUsageView('workspace')}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-button transition-all duration-200 ${\n usageView === 'workspace'\n ? 'bg-bg-surface text-text-primary shadow-elevation-1 font-medium'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n <Layers className=\"size-3.5\" />\n Workspace\n </button>\n <button\n type=\"button\"\n onClick={() => setUsageView('global')}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-button transition-all duration-200 ${\n usageView === 'global'\n ? 'bg-bg-surface text-text-primary shadow-elevation-1 font-medium'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n <Package className=\"size-3.5\" />\n Global\n </button>\n </div>\n\n {/* Billing Account Selector — only shown for global view */}\n {usageView === 'global' && billingAccounts.length > 1 && (\n <select\n value={effectiveBillingAccountId || ''}\n onChange={(e) => setSelectedBillingAccountId(e.target.value)}\n className=\"px-3 py-2 text-sm border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] w-full sm:min-w-[200px]\"\n >\n {billingAccounts.map((account) => (\n <option key={account.id} value={account.id}>\n {account.name}\n </option>\n ))}\n </select>\n )}\n\n {/* Refresh Button */}\n <button\n type=\"button\"\n onClick={() => (usageView === 'workspace' ? undefined : refetch())}\n className=\"p-2 border border-border-subtle rounded-button text-text-secondary hover:text-text-primary hover:bg-bg-sunken hover:border-border-strong transition-all duration-200\"\n aria-label=\"Refresh\"\n title=\"Refresh\"\n >\n <RefreshCw className=\"size-4\" />\n </button>\n </div>\n </div>\n\n {/* Workspace Usage Section */}\n {usageView === 'workspace' && workspaceId && (\n <WorkspaceUsageSection workspaceId={workspaceId} />\n )}\n\n {usageView === 'workspace' && !workspaceId && (\n <div className=\"text-center py-12 text-text-secondary text-sm\">\n No workspace context available.\n </div>\n )}\n\n {/* Global Usage Section — Search and Filters */}\n {usageView === 'global' && (\n <>\n {/* Search and Filters */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center\">\n {/* Search Input */}\n <div className=\"relative flex-1 max-w-md\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-muted\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder={`Search ${viewMode === 'past' ? 'past' : viewMode === 'all' ? 'all' : 'active'} quotas...`}\n className=\"w-full pl-9 pr-3 py-2 text-sm border border-border-subtle rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {(searchQuery || isSearching) && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery('')}\n aria-label=\"Clear search\"\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-bg-sunken\"\n >\n {isSearching ? (\n <RefreshCw className=\"size-3 text-text-muted animate-spin\" />\n ) : (\n <X className=\"size-3 text-text-muted\" />\n )}\n </button>\n )}\n </div>\n\n {/* View Mode Tabs */}\n <div className=\"flex items-center bg-bg-sunken rounded-lg p-1 border border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => setViewMode('active')}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-button transition-all duration-200 ${\n viewMode === 'active'\n ? 'bg-bg-surface text-text-primary shadow-elevation-1 font-medium'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n <Package className=\"size-3.5\" />\n Active\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('past')}\n disabled={isPastSubscriptionsLoading && viewMode !== 'past'}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-button transition-all duration-200 ${\n viewMode === 'past'\n ? 'bg-bg-surface text-text-primary shadow-elevation-1 font-medium'\n : 'text-text-secondary hover:text-text-primary'\n } ${isPastSubscriptionsLoading && viewMode !== 'past' ? 'opacity-70' : ''}`}\n >\n {isPastSubscriptionsLoading && viewMode === 'past' ? (\n <RefreshCw className=\"size-3.5 animate-spin\" />\n ) : (\n <Clock className=\"size-3.5\" />\n )}\n Past\n </button>\n </div>\n\n {/* Active Filters Indicator */}\n {hasActiveFilters && (\n <button\n type=\"button\"\n onClick={clearFilters}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary border border-border-subtle rounded-button hover:bg-bg-sunken transition-colors\"\n >\n <X className=\"size-3\" />\n Clear filters\n </button>\n )}\n </div>\n\n {/* Billing Account Info */}\n {selectedAccount && (\n <div className=\"text-sm text-text-secondary\">\n Viewing usage for{' '}\n <span className=\"font-medium text-text-primary\">{selectedAccount.name}</span>\n </div>\n )}\n\n {/* Overview Stats - Clickable to filter */}\n {dashboard && (\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <StatCard\n label=\"Total Quotas\"\n value={dashboard.totalQuotas}\n icon={<Layers className=\"size-5\" />}\n color=\"blue\"\n isActive={statusFilter === 'all'}\n onClick={() => setStatusFilter('all')}\n />\n <StatCard\n label=\"Healthy\"\n value={dashboard.healthyQuotas}\n icon={<CheckCircle className=\"size-5\" />}\n color=\"emerald\"\n isActive={statusFilter === 'healthy'}\n onClick={() => toggleStatusFilter('healthy')}\n />\n <StatCard\n label=\"Warning\"\n value={dashboard.warningQuotas}\n icon={<AlertCircle className=\"size-5\" />}\n color=\"amber\"\n isActive={statusFilter === 'warning'}\n onClick={() => toggleStatusFilter('warning')}\n />\n <StatCard\n label=\"Critical / Exhausted\"\n value={dashboard.criticalQuotas + dashboard.exhaustedQuotas}\n icon={<AlertTriangle className=\"size-5\" />}\n color=\"red\"\n isActive={statusFilter === 'critical'}\n onClick={() => toggleStatusFilter('critical')}\n />\n </div>\n )}\n\n {/* Request More Quota Banner — shown when any quotas are warning or critical/exhausted */}\n {dashboard &&\n (dashboard.warningQuotas > 0 ||\n dashboard.criticalQuotas > 0 ||\n dashboard.exhaustedQuotas > 0) && (\n <div\n className={`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between p-4 rounded-lg border ${\n dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0\n ? `${statusTokens.error.bg} ${statusTokens.error.border}`\n : `${statusTokens.warning.bg} ${statusTokens.warning.border}`\n }`}\n >\n <div className=\"flex items-center gap-3\">\n {dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0 ? (\n <XCircle className={`size-5 shrink-0 ${statusTokens.error.icon}`} />\n ) : (\n <AlertCircle className={`size-5 shrink-0 ${statusTokens.warning.icon}`} />\n )}\n <div>\n <p\n className={`text-sm font-medium ${\n dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0\n ? statusTokens.error.text\n : statusTokens.warning.text\n }`}\n >\n {dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0\n ? `${dashboard.criticalQuotas + dashboard.exhaustedQuotas} quota${dashboard.criticalQuotas + dashboard.exhaustedQuotas !== 1 ? 's' : ''} exhausted or critical`\n : `${dashboard.warningQuotas} quota${dashboard.warningQuotas !== 1 ? 's' : ''} approaching the limit`}\n </p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n Contact support to request a quota increase for your plan.\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate('/support/tickets')}\n className={`shrink-0 px-4 py-2 text-sm font-medium rounded-button border transition-colors duration-200 ${\n dashboard.criticalQuotas > 0 || dashboard.exhaustedQuotas > 0\n ? 'bg-action-danger-bg text-action-danger-text border-action-danger-bg hover:bg-action-danger-bgHover'\n : 'border-status-warning-border text-status-warning-text hover:bg-status-warning-bg-subtle'\n }`}\n >\n Request more quota\n </button>\n </div>\n )}\n\n {/* Filter Results Info */}\n {(hasActiveFilters || viewMode !== 'active') && (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary flex-wrap\">\n <span>\n Showing{' '}\n {displayedSubscriptions.active.reduce(\n (acc, s) => acc + s.aggregatedQuotas.length,\n 0\n ) +\n displayedSubscriptions.past.reduce(\n (acc, s) => acc + s.aggregatedQuotas.length,\n 0\n ) +\n (viewMode !== 'past' ? filteredPooledQuotas.length : 0)}{' '}\n quotas\n </span>\n <span className=\"px-2 py-0.5 bg-[var(--color-accent-soft)] text-text-link rounded text-xs capitalize\">\n {viewMode === 'active' ? 'Active' : viewMode === 'past' ? 'Past' : 'All'}{' '}\n subscriptions\n </span>\n {statusFilter !== 'all' && (\n <span className=\"px-2 py-0.5 bg-bg-sunken text-text-secondary rounded text-xs capitalize\">\n {statusFilter === 'critical' ? 'Critical / Exhausted' : statusFilter}\n </span>\n )}\n {searchQuery && (\n <span className=\"px-2 py-0.5 bg-bg-sunken text-text-secondary rounded text-xs\">\n “{searchQuery}”\n </span>\n )}\n </div>\n )}\n\n {/* Active Subscriptions Section */}\n {displayedSubscriptions.active.length > 0 && (\n <section>\n <h2 className=\"text-lg font-semibold text-text-primary mb-4 flex items-center gap-2\">\n <Package className=\"size-5 text-text-link\" />\n {viewMode === 'all' ? 'Active Subscription Quotas' : 'Subscription Quotas'}\n </h2>\n <div className=\"space-y-4\">\n {displayedSubscriptions.active.map((subscription) => (\n <SubscriptionSection\n key={subscription.id}\n subscription={subscription}\n onQuotaClick={setSelectedQuota}\n defaultExpanded={true}\n />\n ))}\n </div>\n </section>\n )}\n\n {/* Past Subscriptions Section (when viewMode is 'past' or 'all') */}\n {displayedSubscriptions.past.length > 0 && (\n <section>\n <h2 className=\"text-lg font-semibold text-text-primary mb-4 flex items-center gap-2\">\n <Clock className=\"size-5 text-text-muted\" />\n Past Subscription Quotas\n <span className=\"text-xs font-normal text-text-secondary\">\n (Canceled / Expired / Upgraded)\n </span>\n {pastSubscriptionsCount > 0 && (\n <span className=\"text-xs font-normal px-2 py-0.5 bg-bg-sunken text-text-secondary rounded-full tabular-nums\">\n {displayedSubscriptions.past.length}\n {hasMorePastSubscriptions ? '+' : ''} of {pastSubscriptionsCount}\n </span>\n )}\n </h2>\n <div className=\"space-y-4\">\n {displayedSubscriptions.past.map((subscription) => (\n <SubscriptionSection\n key={subscription.id}\n subscription={subscription}\n onQuotaClick={setSelectedQuota}\n defaultExpanded={viewMode === 'past'}\n />\n ))}\n\n {/* Load More Button */}\n {hasMorePastSubscriptions && (\n <div className=\"flex justify-center pt-2\">\n <button\n type=\"button\"\n onClick={loadMorePastSubscriptions}\n disabled={isLoadingMorePast}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-text-secondary hover:text-text-primary border border-border-subtle rounded-button hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n {isLoadingMorePast ? (\n <>\n <RefreshCw className=\"size-4 animate-spin\" />\n Loading…\n </>\n ) : (\n <>\n <ChevronDown className=\"size-4\" />\n Load more past subscriptions\n </>\n )}\n </button>\n </div>\n )}\n </div>\n </section>\n )}\n\n {/* Pooled Quotas Section - Only show for active/all views (pooled quotas are account-level) */}\n {viewMode !== 'past' && filteredPooledQuotas.length > 0 && (\n <section>\n <h2 className=\"text-lg font-semibold text-text-primary mb-4 flex items-center gap-2\">\n <Layers className={`size-5 ${statusTokens.info.icon}`} />\n Pooled Quotas\n <span className=\"text-xs font-normal text-text-secondary\">\n (Shared across all products)\n </span>\n </h2>\n <QuotaTable\n quotas={filteredPooledQuotas}\n showPlanColumn={false}\n onQuotaClick={setSelectedQuota}\n />\n </section>\n )}\n\n {/* Empty State - No quotas at all (when viewing active) */}\n {viewMode === 'active' &&\n activeSubscriptions.length === 0 &&\n pooledQuotas.length === 0 &&\n !hasActiveFilters && (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border-seam rounded-card\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <Layers className=\"size-6 text-text-link\" />\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">\n No active quotas found\n </h3>\n <p className=\"text-sm text-text-secondary mt-1 text-center max-w-sm\">\n Quotas will appear here once you have active subscriptions or pooled allocations.\n </p>\n </div>\n )}\n\n {/* Empty State - No past subscriptions */}\n {viewMode === 'past' &&\n pastSubscriptions.length === 0 &&\n !hasActiveFilters &&\n !isPastSubscriptionsLoading && (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border-seam rounded-card\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <Clock className=\"size-6 text-text-link\" />\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">No past subscriptions</h3>\n <p className=\"text-sm text-text-secondary mt-1 text-center max-w-sm\">\n You don't have any canceled, expired, or upgraded subscriptions yet.\n </p>\n <button\n type=\"button\"\n onClick={() => setViewMode('active')}\n className=\"mt-4 px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n View active subscriptions\n </button>\n </div>\n )}\n\n {/* Loading State for Past Subscriptions */}\n {viewMode === 'past' && isPastSubscriptionsLoading && (\n <div className=\"space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-24 bg-bg-sunken animate-pulse rounded-card\" />\n ))}\n </div>\n )}\n\n {/* Empty State - No results from search/filter */}\n {hasActiveFilters &&\n displayedSubscriptions.active.length === 0 &&\n displayedSubscriptions.past.length === 0 &&\n (viewMode === 'past' || filteredPooledQuotas.length === 0) && (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border-seam rounded-card\">\n <div className=\"size-12 rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n {statusFilter !== 'all' && !searchQuery ? (\n <StatusIcon\n status={\n (statusFilter === 'healthy'\n ? 'HEALTHY'\n : statusFilter === 'warning'\n ? 'WARNING'\n : 'CRITICAL') as QuotaStatus\n }\n className=\"size-6\"\n />\n ) : (\n <Search className=\"size-6 text-text-link\" />\n )}\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">\n {statusFilter !== 'all' && !searchQuery\n ? `No ${statusFilter === 'critical' ? 'critical or exhausted' : statusFilter} quotas`\n : 'No matching quotas'}\n </h3>\n <p className=\"text-sm text-text-secondary mt-1 text-center max-w-sm\">\n {statusFilter !== 'all' && !searchQuery ? (\n <>\n All your quotas are in a different status.\n {dashboard && (\n <span className=\"block mt-1\">\n You have {dashboard.healthyQuotas} healthy, {dashboard.warningQuotas}{' '}\n warning, and {dashboard.criticalQuotas + dashboard.exhaustedQuotas}{' '}\n critical quotas.\n </span>\n )}\n </>\n ) : (\n <>\n No quotas match your current search or filter criteria in{' '}\n {viewMode === 'past' ? 'past' : viewMode === 'all' ? 'any' : 'active'}{' '}\n subscriptions.\n {viewMode === 'active' && !statusFilter.includes('all') && (\n <span className=\"block mt-1\">\n Try switching to “Past” or “All” to search across\n more subscriptions.\n </span>\n )}\n </>\n )}\n </p>\n <div className=\"flex gap-2 mt-4\">\n <button\n type=\"button\"\n onClick={clearFilters}\n className=\"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 {statusFilter !== 'all' ? 'Show all quotas' : 'Clear filters'}\n </button>\n {viewMode === 'active' && searchQuery && (\n <button\n type=\"button\"\n onClick={() => setViewMode('all')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Search all\n </button>\n )}\n </div>\n </div>\n )}\n </>\n )}\n\n {/* Quota Detail Modal */}\n {selectedQuota && (\n <QuotaDetailModal quota={selectedQuota} onClose={() => setSelectedQuota(null)} />\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { cn as t } from "@burdenoff/fe-libs/shared/utils";
|
|
|
4
4
|
function n({ children: n, className: r, interactive: i = !1, selected: a = !1, as: o, onClick: s, ...c }) {
|
|
5
5
|
return /* @__PURE__ */ e(o ?? "div", {
|
|
6
6
|
onClick: s,
|
|
7
|
-
className: t("rounded-card border border-border-seam bg-bg-surface shadow-elevation-1", i && "transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] shadow-[var(--shadow-pop)]
|
|
7
|
+
className: t("rounded-card border border-border-seam bg-bg-surface shadow-elevation-1", i && "transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] shadow-[var(--shadow-pop)] hover:shadow-elevation-2 [@media(hover:none)]:", a && "ring-1 ring-[var(--color-focus-ring)] border-border-strong", s && "cursor-pointer text-left", r),
|
|
8
8
|
...c,
|
|
9
9
|
children: n
|
|
10
10
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Card.js","names":[],"sources":["../../../../src/billing/shared/ui/Card.tsx"],"sourcesContent":["/**\n * Boff UI — Card primitive\n *\n * The workhorse surface for the billing MFE. Renders the canonical Boff UI\n * card recipe: `rounded-card border border-border-subtle bg-bg-surface\n * shadow-elevation-1`. Set `interactive` for the hover-lift treatment and\n * `selected` for the accent-soft selected state.\n *\n * Visual-only primitive — no business logic.\n */\n\nimport type { ElementType, ReactNode } from 'react';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\ninterface CardProps {\n children: ReactNode;\n className?: string;\n /** Apply hover lift + stronger border/elevation (use for clickable cards). */\n interactive?: boolean;\n /** Apply the accent-soft selected ring/wash. */\n selected?: boolean;\n /** Render as a different element (e.g. 'section', 'article'). Defaults to 'div'. */\n as?: ElementType;\n onClick?: () => void;\n /** Passthrough for data-tour and other data-* hooks. */\n [key: `data-${string}`]: string | undefined;\n}\n\nexport function Card({\n children,\n className,\n interactive = false,\n selected = false,\n as,\n onClick,\n ...rest\n}: CardProps) {\n const Component = as ?? 'div';\n return (\n <Component\n onClick={onClick}\n className={cn(\n 'rounded-card border border-border-seam bg-bg-surface shadow-elevation-1',\n interactive &&\n 'transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] shadow-[var(--shadow-pop)]
|
|
1
|
+
{"version":3,"file":"Card.js","names":[],"sources":["../../../../src/billing/shared/ui/Card.tsx"],"sourcesContent":["/**\n * Boff UI — Card primitive\n *\n * The workhorse surface for the billing MFE. Renders the canonical Boff UI\n * card recipe: `rounded-card border border-border-subtle bg-bg-surface\n * shadow-elevation-1`. Set `interactive` for the hover-lift treatment and\n * `selected` for the accent-soft selected state.\n *\n * Visual-only primitive — no business logic.\n */\n\nimport type { ElementType, ReactNode } from 'react';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\ninterface CardProps {\n children: ReactNode;\n className?: string;\n /** Apply hover lift + stronger border/elevation (use for clickable cards). */\n interactive?: boolean;\n /** Apply the accent-soft selected ring/wash. */\n selected?: boolean;\n /** Render as a different element (e.g. 'section', 'article'). Defaults to 'div'. */\n as?: ElementType;\n onClick?: () => void;\n /** Passthrough for data-tour and other data-* hooks. */\n [key: `data-${string}`]: string | undefined;\n}\n\nexport function Card({\n children,\n className,\n interactive = false,\n selected = false,\n as,\n onClick,\n ...rest\n}: CardProps) {\n const Component = as ?? 'div';\n return (\n <Component\n onClick={onClick}\n className={cn(\n 'rounded-card border border-border-seam bg-bg-surface shadow-elevation-1',\n interactive &&\n 'transition-[box-shadow,transform] duration-200 ease-[var(--motion-easing-out)] shadow-[var(--shadow-pop)] hover:shadow-elevation-2 [@media(hover:none)]:',\n selected && 'ring-1 ring-[var(--color-focus-ring)] border-border-strong',\n onClick && 'cursor-pointer text-left',\n className\n )}\n {...rest}\n >\n {children}\n </Component>\n );\n}\n"],"mappings":";;;AA4BA,SAAgB,EAAK,EACnB,aACA,cACA,iBAAc,IACd,cAAW,IACX,OACA,YACA,GAAG,KACS;AAEZ,QACE,kBAFgB,KAAM,OAEtB;EACW;EACT,WAAW,EACT,2EACA,KACE,4JACF,KAAY,8DACZ,KAAW,4BACX,EACD;EACD,GAAI;EAEH;EACS,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-billing",
|
|
3
|
-
"version": "2026.625.
|
|
3
|
+
"version": "2026.625.2",
|
|
4
4
|
"description": "Billing microfrontend for Burdenoff products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@apollo/client": "^4.1.4",
|
|
46
|
-
"@burdenoff/fe-libs": "2026.625.
|
|
46
|
+
"@burdenoff/fe-libs": "2026.625.2",
|
|
47
47
|
"@tanstack/react-query": "^5.90.16",
|
|
48
48
|
"clsx": "^2.1.1",
|
|
49
49
|
"graphql": "^16.10.0",
|