@burdenoff/microfe-billing 2026.530.4 → 2026.531.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/addons/pages/AddonDetailPage.js +85 -84
- package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js +176 -195
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js +408 -206
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js +216 -215
- package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +7 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/TransactionsPage.js +1 -1
- package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js +261 -0
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js.map +1 -0
- package/dist/billing/modules/checkout/components/index.js +1 -0
- package/dist/billing/modules/checkout/hooks/useCheckout.js +2 -0
- package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/index.js +1 -0
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +354 -340
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js +79 -78
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js +29 -20
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/generated/global-operations.js +8 -0
- package/dist/generated/global-operations.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddonsListPage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonsListPage.tsx"],"sourcesContent":["/**\n * Addons Module - Addons List Page\n * Displays all available addons with cart functionality\n */\n\nimport { useState, useMemo, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { ShoppingCart, Plus, Minus, Trash2, X } from 'lucide-react';\nimport { useAddons, useAddonMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useAddonCartStore } from '../store/addonCartStore';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n} from '../../../shared/utils/format';\nimport type { Addon, PlanDuration, SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useDefaultBillingCurrency } 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: 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 limits = feature.quota.limits as { value?: number } | null;\n const limitValue = limits?.value ?? 0;\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\n/**\n * Get the display price and currency for an addon\n * Uses currencyPrices if available, otherwise falls back to base price\n */\nfunction getAddonDisplayPrice(\n addon: Addon,\n displayCurrency: string = 'USD'\n): { price: number; currency: string } {\n // If currencyPrices exists and has the requested currency, use it\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const currencyPricesObj = addon.currencyPrices as Record<string, number>;\n if (currencyPricesObj[displayCurrency]) {\n return {\n price: currencyPricesObj[displayCurrency],\n currency: displayCurrency,\n };\n }\n }\n\n // Fallback to base USD price\n return {\n price: addon.price,\n currency: addon.currency || 'USD',\n };\n}\n\nexport const AddonsListPage: 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 { addons, isLoading, error, refetch } = useAddons();\n const { toggleAddon, isToggling } = useAddonMutations();\n const { addToCart, removeFromCart, updateQuantity, isInCart, getCartItem } = useAddonCartStore();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [showInactive, setShowInactive] = useState(false);\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n const displayCurrency = useDefaultBillingCurrency();\n\n // Permission check\n if (!permissions.canViewAddons) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-muted flex items-center justify-center\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.accessDenied.title', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-muted-foreground max-w-sm\">\n {tr('billing.addons.noViewPermission', \"You don't have permission to view addons.\")}\n </p>\n </div>\n </div>\n );\n }\n\n // Filter addons\n const filteredAddons = addons.filter((addon) => {\n if (!showInactive && !addon.isActive) return false;\n if (billingInterval !== 'all' && addon.duration !== billingInterval) return false;\n return true;\n });\n\n const handleToggleAddon = async (addon: Addon) => {\n if (!permissions.canManageAddons) return;\n try {\n await toggleAddon(addon.id, !addon.isActive);\n await refetch();\n } catch (err) {\n console.error('Failed to toggle addon:', err);\n }\n };\n\n const handleViewAddon = (addon: Addon) => {\n navigateTo(`/addons/${addon.id}`);\n };\n\n const handleEditAddon = (addon: Addon) => {\n navigateTo(`/addons/${addon.id}/edit`);\n };\n\n // Loading state\n if (isLoading && addons.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-muted animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-muted animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-muted 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-muted animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-destructive/10 flex items-center justify-center\">\n <svg\n className=\"size-6 text-destructive\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.addons.failedToLoad', 'Failed to load addons')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-foreground\">\n {tr('billing.addons.title', 'Addons')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.addons.subtitle', 'Enhance your subscription with additional features')}\n </p>\n </div>\n\n {permissions.canManageAddons && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons/new')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 4v16m8-8H4\"\n />\n </svg>\n Create Addon\n </button>\n )}\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border\">\n {/* Billing Interval Toggle */}\n <div className=\"inline-flex rounded-lg border border-border p-1 bg-muted/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-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n >\n Yearly\n </button>\n </div>\n\n {/* Show Inactive Toggle */}\n {permissions.canManageAddons && (\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 text-primary focus:ring-primary\"\n />\n <span className=\"text-muted-foreground\">\n {tr('billing.addons.showInactive', 'Show inactive addons')}\n </span>\n </label>\n )}\n\n {/* View Mode Toggle */}\n <div className=\"ml-auto inline-flex rounded-lg border border-border p-1 bg-muted/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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 {/* Addons Grid/List */}\n {filteredAddons.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border rounded-lg\">\n <div className=\"size-12 rounded-full bg-muted flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-foreground\">\n {tr('billing.addons.noAddonsFound', 'No addons found')}\n </h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {showInactive\n ? tr('billing.addons.noAddonsFiltered', 'No addons match your current filters.')\n : tr('billing.addons.noActiveAddons', 'There are no active addons available.')}\n </p>\n {permissions.canManageAddons && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons/new')}\n className=\"mt-4 px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n {tr('billing.addons.createFirstAddon', 'Create your first addon')}\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 {filteredAddons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n return (\n <AddonCard\n key={addon.id}\n addon={addon}\n onView={handleViewAddon}\n onEdit={handleEditAddon}\n onToggle={handleToggleAddon}\n canManage={permissions.canManageAddons}\n isToggling={isToggling}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n );\n })}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredAddons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n return (\n <AddonRow\n key={addon.id}\n addon={addon}\n onView={handleViewAddon}\n onEdit={handleEditAddon}\n onToggle={handleToggleAddon}\n canManage={permissions.canManageAddons}\n isToggling={isToggling}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n );\n })}\n </div>\n )}\n\n {/* Floating Cart Drawer */}\n <FloatingCartDrawer displayCurrency={displayCurrency} />\n </div>\n );\n};\n\n// ============================================================================\n// Addon Card Component\n// ============================================================================\n\ninterface AddonCardProps {\n addon: Addon;\n onView: (addon: Addon) => void;\n onEdit: (addon: Addon) => void;\n onToggle: (addon: Addon) => void;\n canManage: boolean;\n isToggling: boolean;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst AddonCard: FC<AddonCardProps> = ({\n addon,\n onView,\n onEdit,\n onToggle,\n canManage,\n displayCurrency,\n isToggling,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n}) => {\n const combinedQuotas = useMemo(() => {\n const features = addon.features || [];\n return combineQuotas(features);\n }, [addon.features]);\n\n return (\n <div\n className={`relative border rounded-lg p-6 transition-all hover:shadow-md flex flex-col h-full ${\n addon.isActive\n ? isInCart\n ? 'border-primary bg-primary/5'\n : 'border-border bg-card'\n : 'border-dashed border-muted-foreground/30 bg-muted/30'\n }`}\n >\n {/* Status Badge */}\n {!addon.isActive && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded\">\n Inactive\n </span>\n )}\n\n {/* In Cart Badge */}\n {isInCart && addon.isActive && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-primary text-primary-foreground rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n\n {/* Addon Icon */}\n <div className=\"size-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n {/* Addon Name */}\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-foreground\">{addon.name}</h3>\n <span className=\"text-sm text-muted-foreground\">\n {formatPlanDurationLabel(addon.duration)}\n </span>\n </div>\n\n {/* Price */}\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-bold text-foreground\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-muted-foreground text-sm\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </div>\n\n {/* Combined Quotas */}\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-4\">\n {combinedQuotas.slice(0, 4).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-4 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-foreground\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-foreground\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 4 && (\n <li className=\"text-sm text-muted-foreground pl-6\">\n +{combinedQuotas.length - 4} 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 mt-auto\">\n {/* Add to Cart / Quantity Controls */}\n {addon.isActive &&\n (isInCart ? (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-10 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-destructive hover:bg-destructive/10 transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors flex items-center justify-center gap-2\"\n >\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </button>\n ))}\n\n <button\n type=\"button\"\n onClick={() => onView(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n View Details\n </button>\n\n {canManage && (\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => onEdit(addon)}\n className=\"flex-1 px-3 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n onClick={() => onToggle(addon)}\n disabled={isToggling}\n className={`px-3 py-2 text-sm font-medium border border-border rounded-md transition-colors ${\n addon.isActive\n ? 'text-destructive hover:bg-destructive/10'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {addon.isActive ? 'Deactivate' : 'Activate'}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Addon Row Component (List View)\n// ============================================================================\n\ninterface AddonRowProps {\n addon: Addon;\n onView: (addon: Addon) => void;\n onEdit: (addon: Addon) => void;\n onToggle: (addon: Addon) => void;\n canManage: boolean;\n isToggling: boolean;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst AddonRow: FC<AddonRowProps> = ({\n addon,\n onView,\n onEdit,\n onToggle,\n canManage,\n displayCurrency,\n isToggling,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n}) => {\n const combinedQuotas = useMemo(() => {\n const features = addon.features || [];\n return combineQuotas(features);\n }, [addon.features]);\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-lg transition-all hover:shadow-sm ${\n addon.isActive\n ? isInCart\n ? 'border-primary bg-primary/5'\n : 'border-border bg-card'\n : 'border-dashed border-muted-foreground/30 bg-muted/30'\n }`}\n >\n {/* Addon Icon */}\n <div className=\"size-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n {/* Addon Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-foreground truncate\">{addon.name}</h3>\n {!addon.isActive && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-muted text-muted-foreground rounded\">\n Inactive\n </span>\n )}\n {isInCart && addon.isActive && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-primary text-primary-foreground rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''}\n </p>\n </div>\n\n {/* Price */}\n <div className=\"text-right\">\n <div className=\"font-semibold text-foreground\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n per {formatPlanDuration(addon.duration)}\n </div>\n </div>\n\n {/* Cart Actions */}\n {addon.isActive &&\n (isInCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-8 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-destructive hover:bg-destructive/10 transition-colors ml-1\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"px-3 py-1.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors flex items-center gap-1.5\"\n >\n <ShoppingCart className=\"size-4\" />\n Add\n </button>\n ))}\n\n {/* Actions */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => onView(addon)}\n className=\"px-3 py-1.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n View\n </button>\n {canManage && (\n <>\n <button\n type=\"button\"\n onClick={() => onEdit(addon)}\n className=\"px-3 py-1.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n onClick={() => onToggle(addon)}\n disabled={isToggling}\n className={`px-3 py-1.5 text-sm font-medium border border-border rounded-md transition-colors ${\n addon.isActive\n ? 'text-destructive hover:bg-destructive/10'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {addon.isActive ? 'Deactivate' : 'Activate'}\n </button>\n </>\n )}\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Floating Cart Drawer\n// ============================================================================\n\ninterface FloatingCartDrawerProps {\n displayCurrency: string;\n}\n\nconst FloatingCartDrawer: FC<FloatingCartDrawerProps> = ({ displayCurrency }) => {\n const navigateTo = useBillingNavigate();\n const { items, isCartOpen, toggleCart, closeCart, removeFromCart, updateQuantity, clearCart } =\n useAddonCartStore();\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n\n if (items.length === 0 && !isCartOpen) return null;\n\n return (\n <>\n {/* Floating Cart Button */}\n {!isCartOpen && items.length > 0 && (\n <button\n type=\"button\"\n onClick={toggleCart}\n className=\"fixed bottom-6 right-6 z-50 flex items-center gap-2 px-4 py-3 bg-primary text-primary-foreground rounded-full shadow-lg hover:bg-primary/90 transition-all\"\n >\n <ShoppingCart className=\"size-5\" />\n <span className=\"font-medium\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n <span className=\"px-2 py-0.5 bg-primary-foreground/20 rounded-full text-sm\">\n {(() => {\n // Calculate cart total using display currency prices\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </button>\n )}\n\n {/* Cart Drawer */}\n {isCartOpen && (\n <div className=\"fixed inset-0 z-50 flex items-end sm:items-center justify-center\">\n {/* Backdrop */}\n <div className=\"absolute inset-0 bg-overlay-scrim\" onClick={closeCart} />\n\n {/* Drawer */}\n <div className=\"relative w-full max-w-md max-h-[80vh] bg-card border border-border rounded-t-xl sm:rounded-xl shadow-xl overflow-hidden\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border bg-muted/30\">\n <div className=\"flex items-center gap-2\">\n <ShoppingCart className=\"size-5 text-foreground\" />\n <h3 className=\"font-semibold text-foreground\">Your Cart</h3>\n <span className=\"px-2 py-0.5 text-xs bg-primary text-primary-foreground rounded-full\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={closeCart}\n className=\"p-1.5 rounded hover:bg-muted transition-colors\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n\n {/* Cart Items */}\n <div className=\"overflow-y-auto max-h-[50vh] divide-y divide-border\">\n {items.map(({ addon, quantity }) => (\n <div key={addon.id} className=\"p-4 flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0\">\n <svg\n className=\"size-5 text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-foreground truncate\">{addon.name}</h4>\n <p className=\"text-sm text-muted-foreground\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}{' '}\n / {formatPlanDuration(addon.duration)}\n </p>\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity - 1)}\n className=\"p-1 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{quantity}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity + 1)}\n className=\"p-1 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n </div>\n <div className=\"text-right\">\n <p className=\"font-medium text-foreground\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price * quantity, currency);\n })()}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1.5 rounded text-destructive hover:bg-destructive/10 transition-colors\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ))}\n </div>\n\n {/* Footer */}\n <div className=\"border-t border-border p-4 space-y-3 bg-muted/20\">\n <div className=\"flex items-center justify-between text-lg font-semibold\">\n <span className=\"text-foreground\">Total</span>\n <span className=\"text-foreground\">\n {(() => {\n // Calculate cart total using display currency prices\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={clearCart}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n Clear Cart\n </button>\n <button\n type=\"button\"\n onClick={() => {\n closeCart();\n navigateTo('/checkout/addons');\n }}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Checkout\n </button>\n </div>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AddonsListPage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonsListPage.tsx"],"sourcesContent":["/**\n * Addons Module - Addons List Page\n * Displays all available addons with cart functionality\n */\n\nimport { useState, useMemo, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { ShoppingCart, Plus, Minus, Trash2, X } from 'lucide-react';\nimport { useAddons, useAddonMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useAddonCartStore } from '../store/addonCartStore';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n} from '../../../shared/utils/format';\nimport type { Addon, PlanDuration, SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useDefaultBillingCurrency } from '../../../hooks/useDefaultBillingCurrency';\nimport { getPlanFeatureQuantity } from '../../../shared/utils/planFeatureQuantity';\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: 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\n/**\n * Get the display price and currency for an addon\n * Uses currencyPrices if available, otherwise falls back to base price\n */\nfunction getAddonDisplayPrice(\n addon: Addon,\n displayCurrency: string = 'USD'\n): { price: number; currency: string } {\n // If currencyPrices exists and has the requested currency, use it\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const currencyPricesObj = addon.currencyPrices as Record<string, number>;\n if (currencyPricesObj[displayCurrency]) {\n return {\n price: currencyPricesObj[displayCurrency],\n currency: displayCurrency,\n };\n }\n }\n\n // Fallback to base USD price\n return {\n price: addon.price,\n currency: addon.currency || 'USD',\n };\n}\n\nexport const AddonsListPage: 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 { addons, isLoading, error, refetch } = useAddons();\n const { toggleAddon, isToggling } = useAddonMutations();\n const { addToCart, removeFromCart, updateQuantity, isInCart, getCartItem } = useAddonCartStore();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [showInactive, setShowInactive] = useState(false);\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n const displayCurrency = useDefaultBillingCurrency();\n\n // Permission check\n if (!permissions.canViewAddons) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-muted flex items-center justify-center\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.accessDenied.title', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-muted-foreground max-w-sm\">\n {tr('billing.addons.noViewPermission', \"You don't have permission to view addons.\")}\n </p>\n </div>\n </div>\n );\n }\n\n // Filter addons\n const filteredAddons = addons.filter((addon) => {\n if (!showInactive && !addon.isActive) return false;\n if (billingInterval !== 'all' && addon.duration !== billingInterval) return false;\n return true;\n });\n\n const handleToggleAddon = async (addon: Addon) => {\n if (!permissions.canManageAddons) return;\n try {\n await toggleAddon(addon.id, !addon.isActive);\n await refetch();\n } catch (err) {\n console.error('Failed to toggle addon:', err);\n }\n };\n\n const handleViewAddon = (addon: Addon) => {\n navigateTo(`/addons/${addon.id}`);\n };\n\n const handleEditAddon = (addon: Addon) => {\n navigateTo(`/addons/${addon.id}/edit`);\n };\n\n // Loading state\n if (isLoading && addons.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-muted animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-muted animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-muted 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-muted animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-destructive/10 flex items-center justify-center\">\n <svg\n className=\"size-6 text-destructive\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.addons.failedToLoad', 'Failed to load addons')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-foreground\">\n {tr('billing.addons.title', 'Addons')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.addons.subtitle', 'Enhance your subscription with additional features')}\n </p>\n </div>\n\n {permissions.canManageAddons && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons/new')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 4v16m8-8H4\"\n />\n </svg>\n Create Addon\n </button>\n )}\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border\">\n {/* Billing Interval Toggle */}\n <div className=\"inline-flex rounded-lg border border-border p-1 bg-muted/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-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n >\n Yearly\n </button>\n </div>\n\n {/* Show Inactive Toggle */}\n {permissions.canManageAddons && (\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 text-primary focus:ring-primary\"\n />\n <span className=\"text-muted-foreground\">\n {tr('billing.addons.showInactive', 'Show inactive addons')}\n </span>\n </label>\n )}\n\n {/* View Mode Toggle */}\n <div className=\"ml-auto inline-flex rounded-lg border border-border p-1 bg-muted/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\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 {/* Addons Grid/List */}\n {filteredAddons.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border rounded-lg\">\n <div className=\"size-12 rounded-full bg-muted flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-foreground\">\n {tr('billing.addons.noAddonsFound', 'No addons found')}\n </h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {showInactive\n ? tr('billing.addons.noAddonsFiltered', 'No addons match your current filters.')\n : tr('billing.addons.noActiveAddons', 'There are no active addons available.')}\n </p>\n {permissions.canManageAddons && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons/new')}\n className=\"mt-4 px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n {tr('billing.addons.createFirstAddon', 'Create your first addon')}\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 {filteredAddons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n return (\n <AddonCard\n key={addon.id}\n addon={addon}\n onView={handleViewAddon}\n onEdit={handleEditAddon}\n onToggle={handleToggleAddon}\n canManage={permissions.canManageAddons}\n isToggling={isToggling}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n );\n })}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredAddons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n return (\n <AddonRow\n key={addon.id}\n addon={addon}\n onView={handleViewAddon}\n onEdit={handleEditAddon}\n onToggle={handleToggleAddon}\n canManage={permissions.canManageAddons}\n isToggling={isToggling}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n );\n })}\n </div>\n )}\n\n {/* Floating Cart Drawer */}\n <FloatingCartDrawer displayCurrency={displayCurrency} />\n </div>\n );\n};\n\n// ============================================================================\n// Addon Card Component\n// ============================================================================\n\ninterface AddonCardProps {\n addon: Addon;\n onView: (addon: Addon) => void;\n onEdit: (addon: Addon) => void;\n onToggle: (addon: Addon) => void;\n canManage: boolean;\n isToggling: boolean;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst AddonCard: FC<AddonCardProps> = ({\n addon,\n onView,\n onEdit,\n onToggle,\n canManage,\n displayCurrency,\n isToggling,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n}) => {\n const combinedQuotas = useMemo(() => {\n const features = addon.features || [];\n return combineQuotas(features);\n }, [addon.features]);\n\n return (\n <div\n className={`relative border rounded-lg p-6 transition-all hover:shadow-md flex flex-col h-full ${\n addon.isActive\n ? isInCart\n ? 'border-primary bg-primary/5'\n : 'border-border bg-card'\n : 'border-dashed border-muted-foreground/30 bg-muted/30'\n }`}\n >\n {/* Status Badge */}\n {!addon.isActive && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded\">\n Inactive\n </span>\n )}\n\n {/* In Cart Badge */}\n {isInCart && addon.isActive && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-primary text-primary-foreground rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n\n {/* Addon Icon */}\n <div className=\"size-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n {/* Addon Name */}\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-foreground\">{addon.name}</h3>\n <span className=\"text-sm text-muted-foreground\">\n {formatPlanDurationLabel(addon.duration)}\n </span>\n </div>\n\n {/* Price */}\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-bold text-foreground\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-muted-foreground text-sm\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </div>\n\n {/* Combined Quotas */}\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-4\">\n {combinedQuotas.slice(0, 4).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-4 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-foreground\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-foreground\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 4 && (\n <li className=\"text-sm text-muted-foreground pl-6\">\n +{combinedQuotas.length - 4} 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 mt-auto\">\n {/* Add to Cart / Quantity Controls */}\n {addon.isActive &&\n (isInCart ? (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-10 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-destructive hover:bg-destructive/10 transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors flex items-center justify-center gap-2\"\n >\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </button>\n ))}\n\n <button\n type=\"button\"\n onClick={() => onView(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n View Details\n </button>\n\n {canManage && (\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => onEdit(addon)}\n className=\"flex-1 px-3 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n onClick={() => onToggle(addon)}\n disabled={isToggling}\n className={`px-3 py-2 text-sm font-medium border border-border rounded-md transition-colors ${\n addon.isActive\n ? 'text-destructive hover:bg-destructive/10'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {addon.isActive ? 'Deactivate' : 'Activate'}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Addon Row Component (List View)\n// ============================================================================\n\ninterface AddonRowProps {\n addon: Addon;\n onView: (addon: Addon) => void;\n onEdit: (addon: Addon) => void;\n onToggle: (addon: Addon) => void;\n canManage: boolean;\n isToggling: boolean;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst AddonRow: FC<AddonRowProps> = ({\n addon,\n onView,\n onEdit,\n onToggle,\n canManage,\n displayCurrency,\n isToggling,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n}) => {\n const combinedQuotas = useMemo(() => {\n const features = addon.features || [];\n return combineQuotas(features);\n }, [addon.features]);\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-lg transition-all hover:shadow-sm ${\n addon.isActive\n ? isInCart\n ? 'border-primary bg-primary/5'\n : 'border-border bg-card'\n : 'border-dashed border-muted-foreground/30 bg-muted/30'\n }`}\n >\n {/* Addon Icon */}\n <div className=\"size-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n {/* Addon Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-foreground truncate\">{addon.name}</h3>\n {!addon.isActive && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-muted text-muted-foreground rounded\">\n Inactive\n </span>\n )}\n {isInCart && addon.isActive && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-primary text-primary-foreground rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''}\n </p>\n </div>\n\n {/* Price */}\n <div className=\"text-right\">\n <div className=\"font-semibold text-foreground\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n per {formatPlanDuration(addon.duration)}\n </div>\n </div>\n\n {/* Cart Actions */}\n {addon.isActive &&\n (isInCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-8 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-destructive hover:bg-destructive/10 transition-colors ml-1\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"px-3 py-1.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors flex items-center gap-1.5\"\n >\n <ShoppingCart className=\"size-4\" />\n Add\n </button>\n ))}\n\n {/* Actions */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => onView(addon)}\n className=\"px-3 py-1.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n View\n </button>\n {canManage && (\n <>\n <button\n type=\"button\"\n onClick={() => onEdit(addon)}\n className=\"px-3 py-1.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n onClick={() => onToggle(addon)}\n disabled={isToggling}\n className={`px-3 py-1.5 text-sm font-medium border border-border rounded-md transition-colors ${\n addon.isActive\n ? 'text-destructive hover:bg-destructive/10'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {addon.isActive ? 'Deactivate' : 'Activate'}\n </button>\n </>\n )}\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Floating Cart Drawer\n// ============================================================================\n\ninterface FloatingCartDrawerProps {\n displayCurrency: string;\n}\n\nconst FloatingCartDrawer: FC<FloatingCartDrawerProps> = ({ displayCurrency }) => {\n const navigateTo = useBillingNavigate();\n const { items, isCartOpen, toggleCart, closeCart, removeFromCart, updateQuantity, clearCart } =\n useAddonCartStore();\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n\n if (items.length === 0 && !isCartOpen) return null;\n\n return (\n <>\n {/* Floating Cart Button */}\n {!isCartOpen && items.length > 0 && (\n <button\n type=\"button\"\n onClick={toggleCart}\n className=\"fixed bottom-6 right-6 z-50 flex items-center gap-2 px-4 py-3 bg-primary text-primary-foreground rounded-full shadow-lg hover:bg-primary/90 transition-all\"\n >\n <ShoppingCart className=\"size-5\" />\n <span className=\"font-medium\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n <span className=\"px-2 py-0.5 bg-primary-foreground/20 rounded-full text-sm\">\n {(() => {\n // Calculate cart total using display currency prices\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </button>\n )}\n\n {/* Cart Drawer */}\n {isCartOpen && (\n <div className=\"fixed inset-0 z-50 flex items-end sm:items-center justify-center\">\n {/* Backdrop */}\n <div className=\"absolute inset-0 bg-overlay-scrim\" onClick={closeCart} />\n\n {/* Drawer */}\n <div className=\"relative w-full max-w-md max-h-[80vh] bg-card border border-border rounded-t-xl sm:rounded-xl shadow-xl overflow-hidden\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border bg-muted/30\">\n <div className=\"flex items-center gap-2\">\n <ShoppingCart className=\"size-5 text-foreground\" />\n <h3 className=\"font-semibold text-foreground\">Your Cart</h3>\n <span className=\"px-2 py-0.5 text-xs bg-primary text-primary-foreground rounded-full\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={closeCart}\n className=\"p-1.5 rounded hover:bg-muted transition-colors\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n\n {/* Cart Items */}\n <div className=\"overflow-y-auto max-h-[50vh] divide-y divide-border\">\n {items.map(({ addon, quantity }) => (\n <div key={addon.id} className=\"p-4 flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0\">\n <svg\n className=\"size-5 text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-foreground truncate\">{addon.name}</h4>\n <p className=\"text-sm text-muted-foreground\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}{' '}\n / {formatPlanDuration(addon.duration)}\n </p>\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity - 1)}\n className=\"p-1 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{quantity}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity + 1)}\n className=\"p-1 rounded border border-border hover:bg-muted transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n </div>\n <div className=\"text-right\">\n <p className=\"font-medium text-foreground\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price * quantity, currency);\n })()}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1.5 rounded text-destructive hover:bg-destructive/10 transition-colors\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ))}\n </div>\n\n {/* Footer */}\n <div className=\"border-t border-border p-4 space-y-3 bg-muted/20\">\n <div className=\"flex items-center justify-between text-lg font-semibold\">\n <span className=\"text-foreground\">Total</span>\n <span className=\"text-foreground\">\n {(() => {\n // Calculate cart total using display currency prices\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={clearCart}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n Clear Cart\n </button>\n <button\n type=\"button\"\n onClick={() => {\n closeCart();\n navigateTo('/checkout/addons');\n }}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Checkout\n </button>\n </div>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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"}
|
|
@@ -308,7 +308,13 @@ var x = () => {
|
|
|
308
308
|
children: [
|
|
309
309
|
/* @__PURE__ */ y("td", {
|
|
310
310
|
className: "p-4",
|
|
311
|
-
children: [/* @__PURE__ */ v("
|
|
311
|
+
children: [o.invoiceUrl ? /* @__PURE__ */ v("a", {
|
|
312
|
+
href: o.invoiceUrl,
|
|
313
|
+
target: "_blank",
|
|
314
|
+
rel: "noopener noreferrer",
|
|
315
|
+
className: "font-medium text-foreground hover:text-primary transition-colors",
|
|
316
|
+
children: n(o.invoiceNumber)
|
|
317
|
+
}) : /* @__PURE__ */ v("button", {
|
|
312
318
|
type: "button",
|
|
313
319
|
onClick: s,
|
|
314
320
|
className: "font-medium text-foreground hover:text-primary transition-colors",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InvoicesListPage.js","names":[],"sources":["../../../../../src/billing/modules/billing/pages/InvoicesListPage.tsx"],"sourcesContent":["/**\n * Billing Module - Invoices List Page\n * Displays all invoices for the current tenant\n */\n\nimport { useEffect, useState, useMemo, useRef, type FC } from 'react';\nimport { RotateCcw } from 'lucide-react';\nimport { useSearchParams } from 'react-router-dom';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePaginatedInvoices } from '../hooks/useInvoices';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport {\n useGetBillingAccountsByOrgQuery,\n type GetBillingAccountsByOrgQuery,\n} from '../../../../generated/global-operations';\n\ntype BillingAccountItem = GetBillingAccountsByOrgQuery['getBillingAccountsByOrg'][number];\nimport {\n formatCurrency,\n formatDate,\n formatInvoiceStatus,\n formatInvoiceNumber,\n} from '../../../shared/utils/format';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport { getInvoiceStatusColor, getStatusBadgeClasses } from '../../../shared/utils/status';\nimport type { Invoice, InvoiceStatus } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ntype FilterStatus = 'all' | InvoiceStatus;\n\nexport const InvoicesListPage: 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 [searchParams] = useSearchParams();\n const urlBillingAccountId = searchParams.get('billingAccountId') ?? undefined;\n\n const { data: accountsData } = useGetBillingAccountsByOrgQuery({ fetchPolicy: 'cache-first' });\n const billingAccounts = accountsData?.getBillingAccountsByOrg ?? [];\n\n // undefined = user hasn't explicitly chosen yet → auto-pick default\n // null = user explicitly chose \"All Accounts\"\n // string = user explicitly chose a specific account\n const [explicitAccountId, setExplicitAccountId] = useState<string | null | undefined>(\n urlBillingAccountId\n );\n\n useEffect(() => {\n setExplicitAccountId(urlBillingAccountId);\n }, [urlBillingAccountId]);\n\n const defaultAccountId = useMemo(\n () =>\n (billingAccounts.find((a: BillingAccountItem) => a.isDefault) ?? billingAccounts[0])?.id ??\n undefined,\n [billingAccounts]\n );\n\n // Resolved selection: explicit choice wins; before any choice, use the default\n const selectedAccountId: string | null =\n explicitAccountId !== undefined ? explicitAccountId : (defaultAccountId ?? null);\n\n const [filterStatus, setFilterStatus] = useState<FilterStatus>('all');\n const [searchQuery, setSearchQuery] = useState('');\n const [debouncedSearch, setDebouncedSearch] = useState('');\n const [page, setPage] = useState(1);\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n if (debounceTimer.current) clearTimeout(debounceTimer.current);\n debounceTimer.current = setTimeout(() => {\n setDebouncedSearch(searchQuery);\n setPage(1);\n }, 400);\n return () => {\n if (debounceTimer.current) clearTimeout(debounceTimer.current);\n };\n }, [searchQuery]);\n\n const { invoices, totalPages, isLoading, error, refetch } = usePaginatedInvoices({\n billingAccountId: selectedAccountId ?? undefined,\n status: filterStatus !== 'all' ? (filterStatus as InvoiceStatus) : undefined,\n search: debouncedSearch || undefined,\n page,\n pageSize: 20,\n });\n\n // Permission check\n if (!permissions.canViewInvoices) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-muted flex items-center justify-center\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.accessDenied.title', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-muted-foreground max-w-sm\">\n {tr('billing.invoices.noViewPermission', \"You don't have permission to view invoices.\")}\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (isLoading && invoices.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-muted animate-pulse rounded\" />\n <div className=\"space-y-3\">\n {[1, 2, 3, 4, 5].map((i) => (\n <div key={i} className=\"border border-border rounded-lg p-4\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-muted animate-pulse rounded\" />\n <div className=\"flex-1 space-y-2\">\n <div className=\"h-4 w-32 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-48 bg-muted animate-pulse rounded\" />\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-destructive/10 flex items-center justify-center\">\n <svg\n className=\"size-6 text-destructive\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.invoices.failedToLoad', 'Failed to load invoices')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-foreground\">\n {tr('billing.invoices.title', 'Invoices')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.invoices.subtitle', 'View and manage your billing invoices')}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/refunds')}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n <RotateCcw className=\"size-4\" />\n {tr('billing.invoices.myRefunds', 'My Refunds')}\n </button>\n {billingAccounts.length > 1 && (\n <div className=\"flex items-center gap-2\">\n <label className=\"text-sm text-muted-foreground whitespace-nowrap\">\n Billing Account\n </label>\n <select\n value={selectedAccountId ?? ''}\n onChange={(e) => setExplicitAccountId(e.target.value || null)}\n className=\"text-sm border border-border rounded-md bg-background text-foreground px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"\">All Accounts</option>\n {billingAccounts.map((account: BillingAccountItem) => (\n <option key={account.id ?? ''} value={account.id ?? ''}>\n {account.name ?? account.email ?? account.id}\n {account.isDefault ? ' (Default)' : ''}\n </option>\n ))}\n </select>\n </div>\n )}\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border\">\n {/* Status Filter */}\n <div className=\"inline-flex rounded-lg border border-border p-1 bg-muted/50 flex-wrap\">\n {(['all', 'OPEN', 'PAID', 'DRAFT', 'VOID'] as FilterStatus[]).map((status) => (\n <button\n type=\"button\"\n key={status}\n onClick={() => {\n setFilterStatus(status);\n setPage(1);\n }}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n filterStatus === status\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n >\n {status === 'all' ? 'All' : formatInvoiceStatus(status as InvoiceStatus)}\n </button>\n ))}\n </div>\n\n {/* Search */}\n <div className=\"relative flex-1 min-w-0 w-full sm:max-w-md\">\n <svg\n className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n />\n </svg>\n <input\n type=\"text\"\n placeholder={tr('billing.invoices.searchPlaceholder', 'Search by invoice number...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2 text-sm border border-border rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n </div>\n </div>\n\n {/* Invoices Table */}\n {!isLoading && invoices.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border rounded-lg\">\n <div className=\"size-12 rounded-full bg-muted flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-foreground\">\n {tr('billing.invoices.noInvoicesFound', 'No invoices found')}\n </h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {filterStatus !== 'all' || debouncedSearch\n ? tr('billing.invoices.noInvoicesFiltered', 'No invoices match your filters.')\n : tr('billing.invoices.noInvoicesYet', \"You don't have any invoices yet.\")}\n </p>\n </div>\n ) : (\n <>\n <div className=\"border border-border rounded-lg overflow-hidden\">\n <table className=\"w-full\">\n <thead className=\"bg-muted/50\">\n <tr>\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground\">\n Invoice\n </th>\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground\">\n Status\n </th>\n {!selectedAccountId && billingAccounts.length > 1 && (\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground hidden lg:table-cell\">\n Account\n </th>\n )}\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground hidden sm:table-cell\">\n Date\n </th>\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground hidden md:table-cell\">\n Due Date\n </th>\n <th className=\"text-right px-4 py-3 text-sm font-medium text-muted-foreground\">\n Amount\n </th>\n <th className=\"text-right px-4 py-3 text-sm font-medium text-muted-foreground\">\n Actions\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {invoices.map((invoice) => (\n <InvoiceRow\n key={invoice.id}\n invoice={invoice}\n onView={() =>\n navigateTo(\n withBillingAccountId(`/invoices/${invoice.id}`, invoice.billingAccountId)\n )\n }\n canPay={permissions.canPayInvoice}\n canVoid={permissions.canVoidInvoice}\n showAccount={!selectedAccountId && billingAccounts.length > 1}\n accountName={\n billingAccounts.find(\n (a: BillingAccountItem) => a.id === invoice.billingAccountId\n )?.name ?? null\n }\n />\n ))}\n </tbody>\n </table>\n </div>\n\n {totalPages > 1 && (\n <div className=\"flex items-center justify-between pt-2\">\n <p className=\"text-sm text-muted-foreground\">\n Page {page} of {totalPages}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((p) => Math.max(1, p - 1))}\n disabled={page <= 1}\n className=\"px-3 py-1.5 text-sm font-medium border border-border rounded-md bg-background text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={() => setPage((p) => Math.min(totalPages, p + 1))}\n disabled={page >= totalPages}\n className=\"px-3 py-1.5 text-sm font-medium border border-border rounded-md bg-background text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n </>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Invoice Row Component\n// ============================================================================\n\ninterface InvoiceRowProps {\n invoice: Invoice;\n onView: () => void;\n canPay: boolean;\n canVoid: boolean;\n showAccount?: boolean;\n accountName?: string | null;\n}\n\nconst InvoiceRow: FC<InvoiceRowProps> = ({\n invoice,\n onView,\n canPay,\n canVoid,\n showAccount = false,\n accountName,\n}) => {\n const statusColor = getInvoiceStatusColor(invoice.status);\n const isOpen = invoice.status === ('OPEN' as InvoiceStatus);\n const isDraft = invoice.status === ('DRAFT' as InvoiceStatus);\n\n return (\n <tr className=\"hover:bg-muted/30 transition-colors\">\n <td className=\"p-4\">\n <button\n type=\"button\"\n onClick={onView}\n className=\"font-medium text-foreground hover:text-primary transition-colors\"\n >\n {formatInvoiceNumber(invoice.invoiceNumber)}\n </button>\n {(invoice.itemDescription ?? invoice.subscription?.plan?.name) && (\n <p className=\"text-sm text-muted-foreground mt-0.5\">\n {invoice.itemDescription ?? invoice.subscription?.plan?.name}\n </p>\n )}\n </td>\n <td className=\"p-4\">\n <span\n className={`inline-flex px-2 py-1 text-xs font-medium rounded ${getStatusBadgeClasses(statusColor)}`}\n >\n {formatInvoiceStatus(invoice.status)}\n </span>\n </td>\n {showAccount && (\n <td className=\"p-4 hidden lg:table-cell\">\n {accountName ? (\n <span className=\"inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full bg-muted text-muted-foreground border border-border\">\n {accountName}\n </span>\n ) : (\n <span className=\"text-sm text-muted-foreground\">, </span>\n )}\n </td>\n )}\n <td className=\"p-4 text-sm text-muted-foreground hidden sm:table-cell\">\n {formatDate(invoice.createdAt, 'short')}\n </td>\n <td className=\"p-4 text-sm text-muted-foreground hidden md:table-cell\">\n {invoice.dueDate ? formatDate(invoice.dueDate, 'short') : '—'}\n </td>\n <td className=\"p-4 text-right font-medium text-foreground\">\n {formatCurrency(invoice.total, invoice.currency)}\n </td>\n <td className=\"p-4 text-right\">\n <div className=\"flex items-center justify-end gap-2\">\n {invoice.invoiceUrl ? (\n <a\n href={invoice.invoiceUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted rounded-md transition-colors\"\n >\n View\n </a>\n ) : (\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted rounded-md transition-colors\"\n >\n View\n </button>\n )}\n {invoice.pdfUrl && (\n <a\n href={invoice.pdfUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"px-3 py-1.5 text-sm font-medium text-primary hover:bg-primary/10 rounded-md transition-colors\"\n >\n PDF\n </a>\n )}\n {isOpen && canPay && (\n <button\n type=\"button\"\n className=\"px-3 py-1.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Pay\n </button>\n )}\n {(isOpen || isDraft) && canVoid && (\n <button\n type=\"button\"\n className=\"px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-md transition-colors\"\n >\n Void\n </button>\n )}\n </div>\n </td>\n </tr>\n );\n};\n"],"mappings":";;;;;;;;;;;;;AA8BA,IAAa,UAA6B;CACxC,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAEnC,IAAa,GAAoB,EACjC,IAAc,GAAuB,EACrC,CAAC,KAAgB,GAAiB,EAClC,IAAsB,EAAa,IAAI,mBAAmB,IAAI,KAAA,GAE9D,EAAE,MAAM,MAAiB,EAAgC,EAAE,aAAa,eAAe,CAAC,EACxF,IAAkB,GAAc,2BAA2B,EAAE,EAK7D,CAAC,GAAmB,KAAwB,EAChD,EACD;AAED,SAAgB;AACd,IAAqB,EAAoB;IACxC,CAAC,EAAoB,CAAC;CAEzB,IAAM,IAAmB,SAEpB,EAAgB,MAAM,MAA0B,EAAE,UAAU,IAAI,EAAgB,KAAK,MACtF,KAAA,GACF,CAAC,EAAgB,CAClB,EAGK,IACJ,MAAsB,KAAA,IAAiC,KAAoB,OAAzC,GAE9B,CAAC,GAAc,KAAmB,EAAuB,MAAM,EAC/D,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAiB,KAAsB,EAAS,GAAG,EACpD,CAAC,GAAM,KAAW,EAAS,EAAE,EAC7B,IAAgB,EAA6C,KAAK;AAExE,UACM,EAAc,WAAS,aAAa,EAAc,QAAQ,EAC9D,EAAc,UAAU,iBAAiB;AAEvC,EADA,EAAmB,EAAY,EAC/B,EAAQ,EAAE;IACT,IAAI,QACM;AACX,EAAI,EAAc,WAAS,aAAa,EAAc,QAAQ;KAE/D,CAAC,EAAY,CAAC;CAEjB,IAAM,EAAE,aAAU,eAAY,cAAW,UAAO,eAAY,EAAqB;EAC/E,kBAAkB,KAAqB,KAAA;EACvC,QAAQ,MAAiB,QAA0C,KAAA,IAAjC;EAClC,QAAQ,KAAmB,KAAA;EAC3B;EACA,UAAU;EACX,CAAC;AA2FF,QAxFK,EAAY,kBA+Bb,KAAa,EAAS,WAAW,IAEjC,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAK,WAAU,2CAA4C,CAAA,EAC3D,kBAAC,OAAD;GAAK,WAAU;aACZ;IAAC;IAAG;IAAG;IAAG;IAAG;IAAE,CAAC,KAAK,MACpB,kBAAC,OAAD;IAAa,WAAU;cACrB,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAK,WAAU,0CAA2C,CAAA,EAC1D,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD,EAAK,WAAU,2CAA4C,CAAA,EAC3D,kBAAC,OAAD,EAAK,WAAU,2CAA4C,CAAA,CACvD;QACF;;IACF,EARI,EAQJ,CACN;GACE,CAAA,CACF;MAKN,IAEA,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,SAAQ;MACR,QAAO;gBAEP,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA;MACE,CAAA;KACF,CAAA;IACN,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,iCAAiC,0BAA0B;KAC5D,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eAAiC,EAAM;KAAY,CAAA;IAChE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,GAAS;KACxB,WAAU;eACX;KAEQ,CAAA;IACL;;EACF,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,0BAA0B,WAAW;KACtC,CAAA,EACL,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,6BAA6B,wCAAwC;KACvE,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAW,WAAW;MACrC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAC/B,EAAG,8BAA8B,aAAa,CACxC;SACR,EAAgB,SAAS,KACxB,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OAAO,WAAU;iBAAkD;OAE3D,CAAA,EACR,kBAAC,UAAD;OACE,OAAO,KAAqB;OAC5B,WAAW,MAAM,EAAqB,EAAE,OAAO,SAAS,KAAK;OAC7D,WAAU;iBAHZ,CAKE,kBAAC,UAAD;QAAQ,OAAM;kBAAG;QAAqB,CAAA,EACrC,EAAgB,KAAK,MACpB,kBAAC,UAAD;QAA+B,OAAO,EAAQ,MAAM;kBAApD,CACG,EAAQ,QAAQ,EAAQ,SAAS,EAAQ,IACzC,EAAQ,YAAY,eAAe,GAC7B;UAHI,EAAQ,MAAM,GAGlB,CACT,CACK;SACL;QAEJ;OACF;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eACX;MAAC;MAAO;MAAQ;MAAQ;MAAS;MAAO,CAAoB,KAAK,MACjE,kBAAC,UAAD;MACE,MAAK;MAEL,eAAe;AAEb,OADA,EAAgB,EAAO,EACvB,EAAQ,EAAE;;MAEZ,WAAW,gEACT,MAAiB,IACb,4CACA;gBAGL,MAAW,QAAQ,QAAQ,EAAoB,EAAwB;MACjE,EAZF,EAYE,CACT;KACE,CAAA,EAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,SAAQ;MACR,QAAO;gBAEP,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA;MACE,CAAA,EACN,kBAAC,SAAD;MACE,MAAK;MACL,aAAa,EAAG,sCAAsC,8BAA8B;MACpF,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;MAC/C,WAAU;MACV,CAAA,CACE;OACF;;GAGL,CAAC,KAAa,EAAS,WAAW,IACjC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,MAAK;OACL,SAAQ;OACR,QAAO;iBAEP,kBAAC,QAAD;QACE,eAAc;QACd,gBAAe;QACf,aAAa;QACb,GAAE;QACF,CAAA;OACE,CAAA;MACF,CAAA;KACN,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAG,oCAAoC,oBAAoB;MACzD,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBACV,MAAiB,SAAS,IACvB,EAAG,uCAAuC,kCAAkC,GAC5E,EAAG,kCAAkC,mCAAmC;MAC1E,CAAA;KACA;QAEN,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,SAAD;MAAO,WAAU;gBACf,kBAAC,MAAD,EAAA,UAAA;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAgE;QAEzE,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAAgE;QAEzE,CAAA;OACJ,CAAC,KAAqB,EAAgB,SAAS,KAC9C,kBAAC,MAAD;QAAI,WAAU;kBAAqF;QAE9F,CAAA;OAEP,kBAAC,MAAD;QAAI,WAAU;kBAAqF;QAE9F,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAAqF;QAE9F,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAAiE;QAE1E,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAAiE;QAE1E,CAAA;OACF,EAAA,CAAA;MACC,CAAA,EACR,kBAAC,SAAD;MAAO,WAAU;gBACd,EAAS,KAAK,MACb,kBAAC,GAAD;OAEW;OACT,cACE,EACE,EAAqB,aAAa,EAAQ,MAAM,EAAQ,iBAAiB,CAC1E;OAEH,QAAQ,EAAY;OACpB,SAAS,EAAY;OACrB,aAAa,CAAC,KAAqB,EAAgB,SAAS;OAC5D,aACE,EAAgB,MACb,MAA0B,EAAE,OAAO,EAAQ,iBAC7C,EAAE,QAAQ;OAEb,EAfK,EAAQ,GAeb,CACF;MACI,CAAA,CACF;;IACJ,CAAA,EAEL,IAAa,KACZ,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAb;MAA6C;MACrC;MAAK;MAAK;MACd;QACJ,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,GAAS,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE,CAAC;MACjD,UAAU,KAAQ;MAClB,WAAU;gBACX;MAEQ,CAAA,EACT,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,GAAS,MAAM,KAAK,IAAI,GAAY,IAAI,EAAE,CAAC;MAC1D,UAAU,KAAQ;MAClB,WAAU;gBACX;MAEQ,CAAA,CACL;OACF;MAEP,EAAA,CAAA;GAED;MA/RJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,SAAQ;MACR,QAAO;gBAEP,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA;MACE,CAAA;KACF,CAAA;IACN,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,8BAA8B,gBAAgB;KAC/C,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,qCAAqC,8CAA8C;KACrF,CAAA;IACA;;EACF,CAAA;GAwRN,KAAmC,EACvC,YACA,WACA,WACA,YACA,iBAAc,IACd,qBACI;CACJ,IAAM,IAAc,EAAsB,EAAQ,OAAO,EACnD,IAAS,EAAQ,WAAY,QAC7B,IAAU,EAAQ,WAAY;AAEpC,QACE,kBAAC,MAAD;EAAI,WAAU;YAAd;GACE,kBAAC,MAAD;IAAI,WAAU;cAAd,CACE,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAET,EAAoB,EAAQ,cAAc;KACpC,CAAA,GACP,EAAQ,mBAAmB,EAAQ,cAAc,MAAM,SACvD,kBAAC,KAAD;KAAG,WAAU;eACV,EAAQ,mBAAmB,EAAQ,cAAc,MAAM;KACtD,CAAA,CAEH;;GACL,kBAAC,MAAD;IAAI,WAAU;cACZ,kBAAC,QAAD;KACE,WAAW,qDAAqD,EAAsB,EAAY;eAEjG,EAAoB,EAAQ,OAAO;KAC/B,CAAA;IACJ,CAAA;GACJ,KACC,kBAAC,MAAD;IAAI,WAAU;cACX,IACC,kBAAC,QAAD;KAAM,WAAU;eACb;KACI,CAAA,GAEP,kBAAC,QAAD;KAAM,WAAU;eAAgC;KAAS,CAAA;IAExD,CAAA;GAEP,kBAAC,MAAD;IAAI,WAAU;cACX,EAAW,EAAQ,WAAW,QAAQ;IACpC,CAAA;GACL,kBAAC,MAAD;IAAI,WAAU;cACX,EAAQ,UAAU,EAAW,EAAQ,SAAS,QAAQ,GAAG;IACvD,CAAA;GACL,kBAAC,MAAD;IAAI,WAAU;cACX,EAAe,EAAQ,OAAO,EAAQ,SAAS;IAC7C,CAAA;GACL,kBAAC,MAAD;IAAI,WAAU;cACZ,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,EAAQ,aACP,kBAAC,KAAD;OACE,MAAM,EAAQ;OACd,QAAO;OACP,KAAI;OACJ,WAAU;iBACX;OAEG,CAAA,GAEJ,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBACX;OAEQ,CAAA;MAEV,EAAQ,UACP,kBAAC,KAAD;OACE,MAAM,EAAQ;OACd,QAAO;OACP,KAAI;OACJ,WAAU;iBACX;OAEG,CAAA;MAEL,KAAU,KACT,kBAAC,UAAD;OACE,MAAK;OACL,WAAU;iBACX;OAEQ,CAAA;OAET,KAAU,MAAY,KACtB,kBAAC,UAAD;OACE,MAAK;OACL,WAAU;iBACX;OAEQ,CAAA;MAEP;;IACH,CAAA;GACF"}
|
|
1
|
+
{"version":3,"file":"InvoicesListPage.js","names":[],"sources":["../../../../../src/billing/modules/billing/pages/InvoicesListPage.tsx"],"sourcesContent":["/**\n * Billing Module - Invoices List Page\n * Displays all invoices for the current tenant\n */\n\nimport { useEffect, useState, useMemo, useRef, type FC } from 'react';\nimport { RotateCcw } from 'lucide-react';\nimport { useSearchParams } from 'react-router-dom';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePaginatedInvoices } from '../hooks/useInvoices';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport {\n useGetBillingAccountsByOrgQuery,\n type GetBillingAccountsByOrgQuery,\n} from '../../../../generated/global-operations';\n\ntype BillingAccountItem = GetBillingAccountsByOrgQuery['getBillingAccountsByOrg'][number];\nimport {\n formatCurrency,\n formatDate,\n formatInvoiceStatus,\n formatInvoiceNumber,\n} from '../../../shared/utils/format';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport { getInvoiceStatusColor, getStatusBadgeClasses } from '../../../shared/utils/status';\nimport type { Invoice, InvoiceStatus } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ntype FilterStatus = 'all' | InvoiceStatus;\n\nexport const InvoicesListPage: 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 [searchParams] = useSearchParams();\n const urlBillingAccountId = searchParams.get('billingAccountId') ?? undefined;\n\n const { data: accountsData } = useGetBillingAccountsByOrgQuery({ fetchPolicy: 'cache-first' });\n const billingAccounts = accountsData?.getBillingAccountsByOrg ?? [];\n\n // undefined = user hasn't explicitly chosen yet → auto-pick default\n // null = user explicitly chose \"All Accounts\"\n // string = user explicitly chose a specific account\n const [explicitAccountId, setExplicitAccountId] = useState<string | null | undefined>(\n urlBillingAccountId\n );\n\n useEffect(() => {\n setExplicitAccountId(urlBillingAccountId);\n }, [urlBillingAccountId]);\n\n const defaultAccountId = useMemo(\n () =>\n (billingAccounts.find((a: BillingAccountItem) => a.isDefault) ?? billingAccounts[0])?.id ??\n undefined,\n [billingAccounts]\n );\n\n // Resolved selection: explicit choice wins; before any choice, use the default\n const selectedAccountId: string | null =\n explicitAccountId !== undefined ? explicitAccountId : (defaultAccountId ?? null);\n\n const [filterStatus, setFilterStatus] = useState<FilterStatus>('all');\n const [searchQuery, setSearchQuery] = useState('');\n const [debouncedSearch, setDebouncedSearch] = useState('');\n const [page, setPage] = useState(1);\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n if (debounceTimer.current) clearTimeout(debounceTimer.current);\n debounceTimer.current = setTimeout(() => {\n setDebouncedSearch(searchQuery);\n setPage(1);\n }, 400);\n return () => {\n if (debounceTimer.current) clearTimeout(debounceTimer.current);\n };\n }, [searchQuery]);\n\n const { invoices, totalPages, isLoading, error, refetch } = usePaginatedInvoices({\n billingAccountId: selectedAccountId ?? undefined,\n status: filterStatus !== 'all' ? (filterStatus as InvoiceStatus) : undefined,\n search: debouncedSearch || undefined,\n page,\n pageSize: 20,\n });\n\n // Permission check\n if (!permissions.canViewInvoices) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-muted flex items-center justify-center\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.accessDenied.title', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-muted-foreground max-w-sm\">\n {tr('billing.invoices.noViewPermission', \"You don't have permission to view invoices.\")}\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (isLoading && invoices.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-muted animate-pulse rounded\" />\n <div className=\"space-y-3\">\n {[1, 2, 3, 4, 5].map((i) => (\n <div key={i} className=\"border border-border rounded-lg p-4\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-muted animate-pulse rounded\" />\n <div className=\"flex-1 space-y-2\">\n <div className=\"h-4 w-32 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-48 bg-muted animate-pulse rounded\" />\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-destructive/10 flex items-center justify-center\">\n <svg\n className=\"size-6 text-destructive\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.invoices.failedToLoad', 'Failed to load invoices')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-foreground\">\n {tr('billing.invoices.title', 'Invoices')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.invoices.subtitle', 'View and manage your billing invoices')}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/refunds')}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n <RotateCcw className=\"size-4\" />\n {tr('billing.invoices.myRefunds', 'My Refunds')}\n </button>\n {billingAccounts.length > 1 && (\n <div className=\"flex items-center gap-2\">\n <label className=\"text-sm text-muted-foreground whitespace-nowrap\">\n Billing Account\n </label>\n <select\n value={selectedAccountId ?? ''}\n onChange={(e) => setExplicitAccountId(e.target.value || null)}\n className=\"text-sm border border-border rounded-md bg-background text-foreground px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"\">All Accounts</option>\n {billingAccounts.map((account: BillingAccountItem) => (\n <option key={account.id ?? ''} value={account.id ?? ''}>\n {account.name ?? account.email ?? account.id}\n {account.isDefault ? ' (Default)' : ''}\n </option>\n ))}\n </select>\n </div>\n )}\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border\">\n {/* Status Filter */}\n <div className=\"inline-flex rounded-lg border border-border p-1 bg-muted/50 flex-wrap\">\n {(['all', 'OPEN', 'PAID', 'DRAFT', 'VOID'] as FilterStatus[]).map((status) => (\n <button\n type=\"button\"\n key={status}\n onClick={() => {\n setFilterStatus(status);\n setPage(1);\n }}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n filterStatus === status\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n >\n {status === 'all' ? 'All' : formatInvoiceStatus(status as InvoiceStatus)}\n </button>\n ))}\n </div>\n\n {/* Search */}\n <div className=\"relative flex-1 min-w-0 w-full sm:max-w-md\">\n <svg\n className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n />\n </svg>\n <input\n type=\"text\"\n placeholder={tr('billing.invoices.searchPlaceholder', 'Search by invoice number...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2 text-sm border border-border rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n </div>\n </div>\n\n {/* Invoices Table */}\n {!isLoading && invoices.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border rounded-lg\">\n <div className=\"size-12 rounded-full bg-muted flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-foreground\">\n {tr('billing.invoices.noInvoicesFound', 'No invoices found')}\n </h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {filterStatus !== 'all' || debouncedSearch\n ? tr('billing.invoices.noInvoicesFiltered', 'No invoices match your filters.')\n : tr('billing.invoices.noInvoicesYet', \"You don't have any invoices yet.\")}\n </p>\n </div>\n ) : (\n <>\n <div className=\"border border-border rounded-lg overflow-hidden\">\n <table className=\"w-full\">\n <thead className=\"bg-muted/50\">\n <tr>\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground\">\n Invoice\n </th>\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground\">\n Status\n </th>\n {!selectedAccountId && billingAccounts.length > 1 && (\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground hidden lg:table-cell\">\n Account\n </th>\n )}\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground hidden sm:table-cell\">\n Date\n </th>\n <th className=\"text-left px-4 py-3 text-sm font-medium text-muted-foreground hidden md:table-cell\">\n Due Date\n </th>\n <th className=\"text-right px-4 py-3 text-sm font-medium text-muted-foreground\">\n Amount\n </th>\n <th className=\"text-right px-4 py-3 text-sm font-medium text-muted-foreground\">\n Actions\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {invoices.map((invoice) => (\n <InvoiceRow\n key={invoice.id}\n invoice={invoice}\n onView={() =>\n navigateTo(\n withBillingAccountId(`/invoices/${invoice.id}`, invoice.billingAccountId)\n )\n }\n canPay={permissions.canPayInvoice}\n canVoid={permissions.canVoidInvoice}\n showAccount={!selectedAccountId && billingAccounts.length > 1}\n accountName={\n billingAccounts.find(\n (a: BillingAccountItem) => a.id === invoice.billingAccountId\n )?.name ?? null\n }\n />\n ))}\n </tbody>\n </table>\n </div>\n\n {totalPages > 1 && (\n <div className=\"flex items-center justify-between pt-2\">\n <p className=\"text-sm text-muted-foreground\">\n Page {page} of {totalPages}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((p) => Math.max(1, p - 1))}\n disabled={page <= 1}\n className=\"px-3 py-1.5 text-sm font-medium border border-border rounded-md bg-background text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={() => setPage((p) => Math.min(totalPages, p + 1))}\n disabled={page >= totalPages}\n className=\"px-3 py-1.5 text-sm font-medium border border-border rounded-md bg-background text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n </>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Invoice Row Component\n// ============================================================================\n\ninterface InvoiceRowProps {\n invoice: Invoice;\n onView: () => void;\n canPay: boolean;\n canVoid: boolean;\n showAccount?: boolean;\n accountName?: string | null;\n}\n\nconst InvoiceRow: FC<InvoiceRowProps> = ({\n invoice,\n onView,\n canPay,\n canVoid,\n showAccount = false,\n accountName,\n}) => {\n const statusColor = getInvoiceStatusColor(invoice.status);\n const isOpen = invoice.status === ('OPEN' as InvoiceStatus);\n const isDraft = invoice.status === ('DRAFT' as InvoiceStatus);\n\n return (\n <tr className=\"hover:bg-muted/30 transition-colors\">\n <td className=\"p-4\">\n {invoice.invoiceUrl ? (\n <a\n href={invoice.invoiceUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"font-medium text-foreground hover:text-primary transition-colors\"\n >\n {formatInvoiceNumber(invoice.invoiceNumber)}\n </a>\n ) : (\n <button\n type=\"button\"\n onClick={onView}\n className=\"font-medium text-foreground hover:text-primary transition-colors\"\n >\n {formatInvoiceNumber(invoice.invoiceNumber)}\n </button>\n )}\n {(invoice.itemDescription ?? invoice.subscription?.plan?.name) && (\n <p className=\"text-sm text-muted-foreground mt-0.5\">\n {invoice.itemDescription ?? invoice.subscription?.plan?.name}\n </p>\n )}\n </td>\n <td className=\"p-4\">\n <span\n className={`inline-flex px-2 py-1 text-xs font-medium rounded ${getStatusBadgeClasses(statusColor)}`}\n >\n {formatInvoiceStatus(invoice.status)}\n </span>\n </td>\n {showAccount && (\n <td className=\"p-4 hidden lg:table-cell\">\n {accountName ? (\n <span className=\"inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full bg-muted text-muted-foreground border border-border\">\n {accountName}\n </span>\n ) : (\n <span className=\"text-sm text-muted-foreground\">, </span>\n )}\n </td>\n )}\n <td className=\"p-4 text-sm text-muted-foreground hidden sm:table-cell\">\n {formatDate(invoice.createdAt, 'short')}\n </td>\n <td className=\"p-4 text-sm text-muted-foreground hidden md:table-cell\">\n {invoice.dueDate ? formatDate(invoice.dueDate, 'short') : '—'}\n </td>\n <td className=\"p-4 text-right font-medium text-foreground\">\n {formatCurrency(invoice.total, invoice.currency)}\n </td>\n <td className=\"p-4 text-right\">\n <div className=\"flex items-center justify-end gap-2\">\n {invoice.invoiceUrl ? (\n <a\n href={invoice.invoiceUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted rounded-md transition-colors\"\n >\n View\n </a>\n ) : (\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted rounded-md transition-colors\"\n >\n View\n </button>\n )}\n {invoice.pdfUrl && (\n <a\n href={invoice.pdfUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"px-3 py-1.5 text-sm font-medium text-primary hover:bg-primary/10 rounded-md transition-colors\"\n >\n PDF\n </a>\n )}\n {isOpen && canPay && (\n <button\n type=\"button\"\n className=\"px-3 py-1.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Pay\n </button>\n )}\n {(isOpen || isDraft) && canVoid && (\n <button\n type=\"button\"\n className=\"px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-md transition-colors\"\n >\n Void\n </button>\n )}\n </div>\n </td>\n </tr>\n );\n};\n"],"mappings":"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"}
|
|
@@ -311,7 +311,7 @@ var F = {
|
|
|
311
311
|
return t?.zohoBooksInvoiceUrl ? "Zoho Books" : t?.invoiceUrl || t?.invoicePdf ? "Stripe" : null;
|
|
312
312
|
}, Q = (e) => {
|
|
313
313
|
let t = X(e);
|
|
314
|
-
t
|
|
314
|
+
t && (u("light"), d(t));
|
|
315
315
|
};
|
|
316
316
|
if (!E.canViewPayments) return /* @__PURE__ */ M(e, { message: h("billing.transactions.noPermission", "You don't have permission to view transaction history.") });
|
|
317
317
|
if (!C) return /* @__PURE__ */ M("div", {
|