@burdenoff/microfe-billing 2026.821.1 → 2026.825.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/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/BillingRoot.js.map +1 -1
- package/dist/billing/BillingUserRoutes.js.map +1 -1
- package/dist/billing/hooks/useBillingAccountSelection.js.map +1 -1
- package/dist/billing/hooks/useBillingEventEmitter.js.map +1 -1
- package/dist/billing/hooks/useBillingNavigate.js.map +1 -1
- package/dist/billing/hooks/useBillingPermissions.js.map +1 -1
- package/dist/billing/hooks/useDefaultBillingCurrency.js.map +1 -1
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js.map +1 -1
- package/dist/billing/modules/addons/hooks/useAddons.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
- package/dist/billing/modules/addons/store/addonCartStore.js.map +1 -1
- package/dist/billing/modules/affiliate/api.js.map +1 -1
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +107 -107
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js +80 -81
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useInvoices.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useRefunds.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useTransactions.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/UserRefundsPage.js +127 -127
- package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/NativeCheckoutView.js.map +1 -1
- package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
- package/dist/billing/modules/checkout/components/PromoCodeInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js.map +1 -1
- package/dist/billing/modules/checkout/components/StoreShippingSection.js.map +1 -1
- package/dist/billing/modules/checkout/components/StripePaymentForm.js.map +1 -1
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js +0 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useNativeCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/usePaymentProviders.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/usePaymentStatusPolling.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useStoreShipping.js.map +1 -1
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +412 -395
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/checkout/utils/storeOrder.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
- package/dist/billing/modules/credits/hooks/useCredits.js.map +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/hooks/useBillingOverviewData.js.map +1 -1
- package/dist/billing/modules/dashboard/hooks/useDashboard.js.map +1 -1
- package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
- package/dist/billing/modules/earnings/api.js.map +1 -1
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js +145 -145
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/EarningsPage.js +133 -133
- package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +7 -9
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutsPage.js +95 -95
- package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
- package/dist/billing/modules/plans/constants/featuredQuotas.js.map +1 -1
- package/dist/billing/modules/plans/hooks/usePlans.js.map +1 -1
- package/dist/billing/modules/plans/hooks/useQuotas.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
- package/dist/billing/modules/quota/hooks/useQuotaTemplates.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaAssignFreePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
- package/dist/billing/modules/store/pages/StoreCheckoutPage.js +5 -6
- package/dist/billing/modules/store/pages/StoreCheckoutPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js.map +1 -1
- package/dist/billing/modules/subscriptions/hooks/useSubscriptions.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/modules/usage/hooks/useUsage.js +14 -9
- package/dist/billing/modules/usage/hooks/useUsage.js.map +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js +481 -453
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/providers/BillingProvider.js.map +1 -1
- package/dist/billing/shared/components/AccessDenied.js.map +1 -1
- package/dist/billing/shared/components/ActorIdentity.js.map +1 -1
- package/dist/billing/shared/components/CurrencyPriceOverridesEditor.js.map +1 -1
- package/dist/billing/shared/components/EmptyState.js.map +1 -1
- package/dist/billing/shared/components/PageHeader.js.map +1 -1
- package/dist/billing/shared/components/ServerError.js.map +1 -1
- package/dist/billing/shared/components/StatCard.js.map +1 -1
- package/dist/billing/shared/hooks/useActorProfiles.js.map +1 -1
- package/dist/billing/shared/types/graphql.js.map +1 -1
- package/dist/billing/shared/ui/Card.js.map +1 -1
- package/dist/billing/shared/ui/MetricCard.js.map +1 -1
- package/dist/billing/shared/ui/ProgressBar.js.map +1 -1
- package/dist/billing/shared/ui/StatusPill.js.map +1 -1
- package/dist/billing/shared/utils/directBillingGraphql.js.map +1 -1
- package/dist/billing/shared/utils/error.js.map +1 -1
- package/dist/billing/shared/utils/format.js.map +1 -1
- package/dist/billing/shared/utils/gatewayGraphql.js.map +1 -1
- package/dist/billing/shared/utils/navigation.js.map +1 -1
- package/dist/billing/shared/utils/paymentTheme.js.map +1 -1
- package/dist/billing/shared/utils/planFeatureQuantity.js.map +1 -1
- package/dist/billing/shared/utils/status.js.map +1 -1
- package/dist/billing/shared/utils/tokens.js.map +1 -1
- package/dist/generated/global-operations.js +5 -1
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.js.map +1 -1
- package/dist/generated/wspace-operations.js +1 -1
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/node_modules/@capacitor/core/dist/index.js +1 -1
- package/dist/node_modules/@capacitor/core/dist/index.js.map +1 -1
- package/package.json +13 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuotaTemplatesListPage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaTemplatesListPage.tsx"],"sourcesContent":["/**\n * Quota Module - Quota Templates List Page\n * Displays all available quota templates for admin management\n */\n\nimport { useState, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useQuotaTemplates, useQuotaTemplateMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { formatDate } from '../../../shared/utils/format';\nimport type { Quota, QuotaType } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\ntype ViewMode = 'grid' | 'list';\n\nexport const QuotaTemplatesListPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { quotas, isLoading, error, refetch } = useQuotaTemplates();\n const { toggleQuota, isToggling } = useQuotaTemplateMutations();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [showInactive, setShowInactive] = useState(false);\n const [quotaTypeFilter, setQuotaTypeFilter] = useState<QuotaType | 'all'>('all');\n\n // Permission check\n if (!permissions.canViewQuotas) {\n return <AccessDenied message=\"You don't have permission to view quotas.\" />;\n }\n\n // Filter quotas\n const filteredQuotas = quotas.filter((quota) => {\n if (!showInactive && !quota.isActive) return false;\n if (quotaTypeFilter !== 'all' && quota.quotaType !== quotaTypeFilter) return false;\n return true;\n });\n\n const handleToggleQuota = async (quota: Quota) => {\n if (!permissions.canManageQuotas) return;\n try {\n await toggleQuota(quota.id, !quota.isActive);\n await refetch();\n } catch (err) {\n console.error('Failed to toggle quota:', err);\n }\n };\n\n const handleViewQuota = (quota: Quota) => {\n navigateTo(`/quotas/${quota.id}`);\n };\n\n const handleEditQuota = (quota: Quota) => {\n navigateTo(`/quotas/${quota.id}/edit`);\n };\n\n // Loading state\n if (isLoading && quotas.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-6 text-status-error-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.quota.failedToLoad', 'Failed to load quotas')}\n </h2>\n <p className=\"text-sm text-text-muted\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <PageHeader\n title={tr('billing.quota.quotaTemplatesTitle', 'Quota Templates')}\n description=\"Manage quota templates that can be assigned to plans and addons\"\n />\n\n {permissions.canManageQuotas && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas/new')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 4v16m8-8H4\"\n />\n </svg>\n Create Quota\n </button>\n )}\n </div>\n\n <PagePurpose>\n {tr(\n 'billing.quota.templatesPurpose',\n 'Quota templates are the reusable limit definitions you attach to plans and add-ons — like \"API calls per month\" or \"storage\". Create and manage them here once, then reference them across multiple plans instead of redefining the same limit each time.'\n )}\n </PagePurpose>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n {/* Quota Type Toggle */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setQuotaTypeFilter('all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n quotaTypeFilter === 'all'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n All\n </button>\n <button\n type=\"button\"\n onClick={() => setQuotaTypeFilter('POOLED' as QuotaType)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n quotaTypeFilter === 'POOLED'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n Pooled\n </button>\n <button\n type=\"button\"\n onClick={() => setQuotaTypeFilter('PRODUCT_SPECIFIC' as QuotaType)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n quotaTypeFilter === 'PRODUCT_SPECIFIC'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n Product Specific\n </button>\n </div>\n\n {/* Show Inactive Toggle */}\n {permissions.canManageQuotas && (\n <label className=\"flex items-center gap-2 text-sm cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <span className=\"text-text-muted\">\n {tr('billing.quota.showInactive', 'Show inactive quotas')}\n </span>\n </label>\n )}\n\n {/* View Mode Toggle */}\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Quotas Grid/List */}\n {filteredQuotas.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={tr('billing.quota.noQuotasFound', 'No quotas found')}\n description={\n showInactive\n ? 'No quotas match your current filters.'\n : 'There are no active quotas available.'\n }\n action={\n permissions.canManageQuotas ? (\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas/new')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Create your first quota\n </button>\n ) : undefined\n }\n />\n ) : viewMode === 'grid' ? (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {filteredQuotas.map((quota) => (\n <QuotaCard\n key={quota.id}\n quota={quota}\n onView={handleViewQuota}\n onEdit={handleEditQuota}\n onToggle={handleToggleQuota}\n canManage={permissions.canManageQuotas}\n isToggling={isToggling}\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredQuotas.map((quota) => (\n <QuotaRow\n key={quota.id}\n quota={quota}\n onView={handleViewQuota}\n onEdit={handleEditQuota}\n onToggle={handleToggleQuota}\n canManage={permissions.canManageQuotas}\n isToggling={isToggling}\n />\n ))}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Quota Card Component\n// ============================================================================\n\ninterface QuotaCardProps {\n quota: Quota;\n onView: (quota: Quota) => void;\n onEdit: (quota: Quota) => void;\n onToggle: (quota: Quota) => void;\n canManage: boolean;\n isToggling: boolean;\n}\n\nconst QuotaCard: FC<QuotaCardProps> = ({\n quota,\n onView,\n onEdit,\n onToggle,\n canManage,\n isToggling,\n}) => {\n const limits = quota.limits as { type?: string; value?: number; unit?: string } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = limits?.value ?? 0;\n const limitUnit = limits?.unit ?? '';\n\n return (\n <div\n className={`relative border rounded-card p-6 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] flex flex-col h-full ${\n quota.isActive\n ? 'border-border-subtle bg-bg-surface'\n : 'border-dashed border-text-muted/30 bg-bg-sunken/30'\n }`}\n >\n {/* Status Badge */}\n {!quota.isActive && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n Inactive\n </span>\n )}\n\n {/* Quota Icon */}\n <div className=\"size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <svg\n className=\"size-5 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n </div>\n\n {/* Quota Name */}\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-text-primary\">{quota.name}</h3>\n <div className=\"flex items-center gap-2 mt-1\">\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded ${\n quota.quotaType === 'POOLED'\n ? 'bg-status-info-bg-subtle text-status-info-text'\n : 'bg-accent-purple-subtle text-text-primary'\n }`}\n >\n {quota.quotaType === 'POOLED' ? 'Pooled' : 'Product Specific'}\n </span>\n {quota.reusable && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-status-success-bg-subtle text-status-success-text rounded\">\n Reusable\n </span>\n )}\n </div>\n </div>\n\n {/* Limits Info */}\n <div className=\"space-y-2 mb-4 flex-1\">\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-muted\">Limit Type:</span>\n <span className=\"font-medium text-text-primary capitalize\">{limitType}</span>\n </div>\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-muted\">Value:</span>\n <span className=\"font-semibold text-text-primary\">\n {limitValue.toLocaleString()}\n {limitUnit && ` ${limitUnit}`}\n </span>\n </div>\n </div>\n\n {/* Created Date */}\n <div className=\"py-3 border-t border-border-subtle text-sm text-text-muted\">\n Created {formatDate(quota.createdAt, 'short')}\n </div>\n\n {/* Actions */}\n <div className=\"space-y-2 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => onView(quota)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n View Details\n </button>\n {canManage && (\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => onEdit(quota)}\n className=\"flex-1 px-3 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n onClick={() => onToggle(quota)}\n disabled={isToggling}\n className={`px-3 py-2 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n quota.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {quota.isActive ? 'Deactivate' : 'Activate'}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Quota Row Component (List View)\n// ============================================================================\n\ninterface QuotaRowProps {\n quota: Quota;\n onView: (quota: Quota) => void;\n onEdit: (quota: Quota) => void;\n onToggle: (quota: Quota) => void;\n canManage: boolean;\n isToggling: boolean;\n}\n\nconst QuotaRow: FC<QuotaRowProps> = ({\n quota,\n onView,\n onEdit,\n onToggle,\n canManage,\n isToggling,\n}) => {\n const limits = quota.limits as { type?: string; value?: number; unit?: string } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = limits?.value ?? 0;\n const limitUnit = limits?.unit ?? '';\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-card shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] ${\n quota.isActive\n ? 'border-border-subtle bg-bg-surface'\n : 'border-dashed border-text-muted/30 bg-bg-sunken/30'\n }`}\n >\n {/* Quota Icon */}\n <div className=\"size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center shrink-0\">\n <svg\n className=\"size-5 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n </div>\n\n {/* Quota Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <h3 className=\"font-semibold text-text-primary truncate\">{quota.name}</h3>\n {!quota.isActive && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n Inactive\n </span>\n )}\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded ${\n quota.quotaType === 'POOLED'\n ? 'bg-status-info-bg-subtle text-status-info-text'\n : 'bg-accent-purple-subtle text-text-primary'\n }`}\n >\n {quota.quotaType === 'POOLED' ? 'Pooled' : 'Product Specific'}\n </span>\n </div>\n <p className=\"text-sm text-text-muted\">\n <span className=\"capitalize\">{limitType}</span> limit: {limitValue.toLocaleString()}\n {limitUnit && ` ${limitUnit}`}\n {quota.reusable && ' (Reusable)'}\n </p>\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => onView(quota)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n View\n </button>\n {canManage && (\n <>\n <button\n type=\"button\"\n onClick={() => onEdit(quota)}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n onClick={() => onToggle(quota)}\n disabled={isToggling}\n className={`px-3 py-1.5 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n quota.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {quota.isActive ? 'Deactivate' : 'Activate'}\n </button>\n </>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"QuotaTemplatesListPage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaTemplatesListPage.tsx"],"sourcesContent":["/**\n * Quota Module - Quota Templates List Page\n * Displays all available quota templates for admin management\n */\n\nimport { useState, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useQuotaTemplates, useQuotaTemplateMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { formatDate } from '../../../shared/utils/format';\nimport type { Quota, QuotaType } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\ntype ViewMode = 'grid' | 'list';\n\nexport const QuotaTemplatesListPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { quotas, isLoading, error, refetch } = useQuotaTemplates();\n const { toggleQuota, isToggling } = useQuotaTemplateMutations();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [showInactive, setShowInactive] = useState(false);\n const [quotaTypeFilter, setQuotaTypeFilter] = useState<QuotaType | 'all'>('all');\n\n // Permission check\n if (!permissions.canViewQuotas) {\n return <AccessDenied message=\"You don't have permission to view quotas.\" />;\n }\n\n // Filter quotas\n const filteredQuotas = quotas.filter((quota) => {\n if (!showInactive && !quota.isActive) return false;\n if (quotaTypeFilter !== 'all' && quota.quotaType !== quotaTypeFilter) return false;\n return true;\n });\n\n const handleToggleQuota = async (quota: Quota) => {\n if (!permissions.canManageQuotas) return;\n try {\n await toggleQuota(quota.id, !quota.isActive);\n await refetch();\n } catch (err) {\n console.error('Failed to toggle quota:', err);\n }\n };\n\n const handleViewQuota = (quota: Quota) => {\n navigateTo(`/quotas/${quota.id}`);\n };\n\n const handleEditQuota = (quota: Quota) => {\n navigateTo(`/quotas/${quota.id}/edit`);\n };\n\n // Loading state\n if (isLoading && quotas.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-6 text-status-error-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.quota.failedToLoad', 'Failed to load quotas')}\n </h2>\n <p className=\"text-sm text-text-muted\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <PageHeader\n title={tr('billing.quota.quotaTemplatesTitle', 'Quota Templates')}\n description=\"Manage quota templates that can be assigned to plans and addons\"\n />\n\n {permissions.canManageQuotas && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas/new')}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 4v16m8-8H4\"\n />\n </svg>\n Create Quota\n </button>\n )}\n </div>\n\n <PagePurpose>\n {tr(\n 'billing.quota.templatesPurpose',\n 'Quota templates are the reusable limit definitions you attach to plans and add-ons — like \"API calls per month\" or \"storage\". Create and manage them here once, then reference them across multiple plans instead of redefining the same limit each time.'\n )}\n </PagePurpose>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n {/* Quota Type Toggle */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setQuotaTypeFilter('all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n quotaTypeFilter === 'all'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n All\n </button>\n <button\n type=\"button\"\n onClick={() => setQuotaTypeFilter('POOLED' as QuotaType)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n quotaTypeFilter === 'POOLED'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n Pooled\n </button>\n <button\n type=\"button\"\n onClick={() => setQuotaTypeFilter('PRODUCT_SPECIFIC' as QuotaType)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n quotaTypeFilter === 'PRODUCT_SPECIFIC'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n >\n Product Specific\n </button>\n </div>\n\n {/* Show Inactive Toggle */}\n {permissions.canManageQuotas && (\n <label className=\"flex items-center gap-2 text-sm cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <span className=\"text-text-muted\">\n {tr('billing.quota.showInactive', 'Show inactive quotas')}\n </span>\n </label>\n )}\n\n {/* View Mode Toggle */}\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-muted hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Quotas Grid/List */}\n {filteredQuotas.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={tr('billing.quota.noQuotasFound', 'No quotas found')}\n description={\n showInactive\n ? 'No quotas match your current filters.'\n : 'There are no active quotas available.'\n }\n action={\n permissions.canManageQuotas ? (\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas/new')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Create your first quota\n </button>\n ) : undefined\n }\n />\n ) : viewMode === 'grid' ? (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {filteredQuotas.map((quota) => (\n <QuotaCard\n key={quota.id}\n quota={quota}\n onView={handleViewQuota}\n onEdit={handleEditQuota}\n onToggle={handleToggleQuota}\n canManage={permissions.canManageQuotas}\n isToggling={isToggling}\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredQuotas.map((quota) => (\n <QuotaRow\n key={quota.id}\n quota={quota}\n onView={handleViewQuota}\n onEdit={handleEditQuota}\n onToggle={handleToggleQuota}\n canManage={permissions.canManageQuotas}\n isToggling={isToggling}\n />\n ))}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Quota Card Component\n// ============================================================================\n\ninterface QuotaCardProps {\n quota: Quota;\n onView: (quota: Quota) => void;\n onEdit: (quota: Quota) => void;\n onToggle: (quota: Quota) => void;\n canManage: boolean;\n isToggling: boolean;\n}\n\nconst QuotaCard: FC<QuotaCardProps> = ({\n quota,\n onView,\n onEdit,\n onToggle,\n canManage,\n isToggling,\n}) => {\n const limits = quota.limits as { type?: string; value?: number; unit?: string } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = limits?.value ?? 0;\n const limitUnit = limits?.unit ?? '';\n\n return (\n <div\n className={`relative border rounded-card p-6 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] flex flex-col h-full ${\n quota.isActive\n ? 'border-border-subtle bg-bg-surface'\n : 'border-dashed border-text-muted/30 bg-bg-sunken/30'\n }`}\n >\n {/* Status Badge */}\n {!quota.isActive && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n Inactive\n </span>\n )}\n\n {/* Quota Icon */}\n <div className=\"size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center mb-4\">\n <svg\n className=\"size-5 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n </div>\n\n {/* Quota Name */}\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-text-primary\">{quota.name}</h3>\n <div className=\"flex items-center gap-2 mt-1\">\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded ${\n quota.quotaType === 'POOLED'\n ? 'bg-status-info-bg-subtle text-status-info-text'\n : 'bg-accent-purple-subtle text-text-primary'\n }`}\n >\n {quota.quotaType === 'POOLED' ? 'Pooled' : 'Product Specific'}\n </span>\n {quota.reusable && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-status-success-bg-subtle text-status-success-text rounded\">\n Reusable\n </span>\n )}\n </div>\n </div>\n\n {/* Limits Info */}\n <div className=\"space-y-2 mb-4 flex-1\">\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-muted\">Limit Type:</span>\n <span className=\"font-medium text-text-primary capitalize\">{limitType}</span>\n </div>\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-muted\">Value:</span>\n <span className=\"font-semibold text-text-primary\">\n {limitValue.toLocaleString()}\n {limitUnit && ` ${limitUnit}`}\n </span>\n </div>\n </div>\n\n {/* Created Date */}\n <div className=\"py-3 border-t border-border-subtle text-sm text-text-muted\">\n Created {formatDate(quota.createdAt, 'short')}\n </div>\n\n {/* Actions */}\n <div className=\"space-y-2 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => onView(quota)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n View Details\n </button>\n {canManage && (\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => onEdit(quota)}\n className=\"flex-1 px-3 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n onClick={() => onToggle(quota)}\n disabled={isToggling}\n className={`px-3 py-2 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n quota.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {quota.isActive ? 'Deactivate' : 'Activate'}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Quota Row Component (List View)\n// ============================================================================\n\ninterface QuotaRowProps {\n quota: Quota;\n onView: (quota: Quota) => void;\n onEdit: (quota: Quota) => void;\n onToggle: (quota: Quota) => void;\n canManage: boolean;\n isToggling: boolean;\n}\n\nconst QuotaRow: FC<QuotaRowProps> = ({\n quota,\n onView,\n onEdit,\n onToggle,\n canManage,\n isToggling,\n}) => {\n const limits = quota.limits as { type?: string; value?: number; unit?: string } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = limits?.value ?? 0;\n const limitUnit = limits?.unit ?? '';\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-card shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] ${\n quota.isActive\n ? 'border-border-subtle bg-bg-surface'\n : 'border-dashed border-text-muted/30 bg-bg-sunken/30'\n }`}\n >\n {/* Quota Icon */}\n <div className=\"size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center shrink-0\">\n <svg\n className=\"size-5 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n </div>\n\n {/* Quota Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <h3 className=\"font-semibold text-text-primary truncate\">{quota.name}</h3>\n {!quota.isActive && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n Inactive\n </span>\n )}\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded ${\n quota.quotaType === 'POOLED'\n ? 'bg-status-info-bg-subtle text-status-info-text'\n : 'bg-accent-purple-subtle text-text-primary'\n }`}\n >\n {quota.quotaType === 'POOLED' ? 'Pooled' : 'Product Specific'}\n </span>\n </div>\n <p className=\"text-sm text-text-muted\">\n <span className=\"capitalize\">{limitType}</span> limit: {limitValue.toLocaleString()}\n {limitUnit && ` ${limitUnit}`}\n {quota.reusable && ' (Reusable)'}\n </p>\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => onView(quota)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n View\n </button>\n {canManage && (\n <>\n <button\n type=\"button\"\n onClick={() => onEdit(quota)}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n onClick={() => onToggle(quota)}\n disabled={isToggling}\n className={`px-3 py-1.5 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n quota.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {quota.isActive ? 'Deactivate' : 'Activate'}\n </button>\n </>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SettingsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/SettingsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Settings Page\n * Billing account settings and configuration\n */\n\nimport { useState, useMemo, useEffect, useCallback, type FC } from 'react';\nimport { useBillingEventEmitter } from '../../../hooks/useBillingEventEmitter';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n OptionalFields,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\nimport {\n useGetBillingAccountSettingsQuery,\n useGetBillingAccountsSettingsQuery,\n useGetDefaultBillingAccountSettingsQuery,\n useUpdateBillingAccountSettingsMutation,\n useSetDefaultBillingAccountSettingsMutation,\n ArchiveBillingAccountSettingsDocument,\n} from '../../../../generated/global-operations';\nimport type { BillingAccount } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n// Helper to get billing account ID from URL (for navigation from Overview page)\nconst getInitialBillingAccountId = (): string | undefined => {\n if (typeof window === 'undefined') return undefined;\n const urlParams = new URLSearchParams(window.location.search);\n return urlParams.get('billingAccountId') || undefined;\n};\n\nexport const SettingsPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const permissions = useBillingPermissions();\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n const { emit } = useBillingEventEmitter();\n\n // State - initialize with URL param if present\n const [selectedBillingAccountId, setSelectedBillingAccountId] = useState<string | undefined>(\n getInitialBillingAccountId\n );\n const [isAccountSelectorOpen, setIsAccountSelectorOpen] = useState(false);\n const [isSaving, setIsSaving] = useState(false);\n const [isArchiving, setIsArchiving] = useState(false);\n const [isArchiveConfirmOpen, setIsArchiveConfirmOpen] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n const [saveSuccess, setSaveSuccess] = useState(false);\n\n // Set default confirmation modal state\n const [isSetDefaultModalOpen, setIsSetDefaultModalOpen] = useState(false);\n const [pendingSave, setPendingSave] = useState(false);\n\n // Form state - includes isDefault toggle\n const [formData, setFormData] = useState({\n name: '',\n email: '',\n billingEmail: '',\n currency: 'USD',\n taxId: '',\n isDefault: false,\n });\n\n // Fetch all billing accounts for the selector (lightweight query without subscriptions)\n const {\n data: accountsData,\n loading: isLoadingAccounts,\n refetch: refetchAccounts,\n } = useGetBillingAccountsSettingsQuery({});\n const billingAccounts = useMemo<BillingAccount[]>(\n () => accountsData?.getBillingAccountsByOrg || [],\n [accountsData?.getBillingAccountsByOrg]\n );\n\n // Fetch default billing account (without subscriptions)\n const {\n data: defaultData,\n loading: isLoadingDefault,\n refetch: refetchDefault,\n } = useGetDefaultBillingAccountSettingsQuery({});\n const defaultBillingAccount: BillingAccount | null =\n defaultData?.getDefaultBillingAccount || null;\n\n // Always fetch selected billing account if we have an ID from URL (without subscriptions)\n const {\n data: selectedData,\n loading: isLoadingSelected,\n refetch: refetchSelected,\n } = useGetBillingAccountSettingsQuery({\n variables: { id: selectedBillingAccountId! },\n skip: !selectedBillingAccountId,\n });\n const selectedAccountData: BillingAccount | null = selectedData?.getBillingAccount || null;\n\n // Mutation for updating billing account\n const [updateBillingAccountMutation] = useUpdateBillingAccountSettingsMutation();\n\n // Mutation for setting default billing account\n const [setDefaultBillingAccountMutation] = useSetDefaultBillingAccountSettingsMutation();\n\n // Determine which account to use - prioritize URL param\n const billingAccount = useMemo(() => {\n // If we have a selected account ID from URL\n if (selectedBillingAccountId) {\n // First try to use the fetched selected account data\n if (selectedAccountData) {\n return selectedAccountData;\n }\n // While loading, try to find from the accounts list\n const fromList = billingAccounts.find((a) => a.id === selectedBillingAccountId);\n if (fromList) {\n return fromList;\n }\n // If still loading and not in list, return null to show loading state\n if (isLoadingSelected) {\n return null;\n }\n }\n // No URL param - use default account\n return defaultBillingAccount;\n }, [\n selectedBillingAccountId,\n selectedAccountData,\n billingAccounts,\n isLoadingSelected,\n defaultBillingAccount,\n ]);\n\n const isLoading =\n isLoadingAccounts || isLoadingDefault || (selectedBillingAccountId && isLoadingSelected);\n\n // Update form when billing account changes\n useEffect(() => {\n if (billingAccount) {\n setFormData({\n name: billingAccount.name || '',\n email: billingAccount.email || '',\n billingEmail: billingAccount.billingEmail || '',\n currency: billingAccount.currency || 'USD',\n taxId: billingAccount.taxId || '',\n isDefault: billingAccount.isDefault || false,\n });\n }\n }, [billingAccount]);\n\n // Handle account selection\n const handleSelectAccount = (account: BillingAccount) => {\n setSelectedBillingAccountId(account.id);\n setIsAccountSelectorOpen(false);\n setSaveSuccess(false);\n setSaveError(null);\n };\n\n // Handle form input changes\n const handleInputChange = (field: keyof typeof formData, value: string) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n setSaveSuccess(false);\n setSaveError(null);\n };\n\n // Find the current default account\n const currentDefaultAccount = useMemo(() => {\n return billingAccounts.find((a) => a.isDefault) || null;\n }, [billingAccounts]);\n\n // Check if form has changes\n const hasChanges = useMemo(() => {\n if (!billingAccount) return false;\n return (\n formData.name !== (billingAccount.name || '') ||\n formData.email !== (billingAccount.email || '') ||\n formData.billingEmail !== (billingAccount.billingEmail || '') ||\n formData.currency !== (billingAccount.currency || 'USD') ||\n formData.taxId !== (billingAccount.taxId || '') ||\n formData.isDefault !== (billingAccount.isDefault || false)\n );\n }, [formData, billingAccount]);\n\n // Check if trying to change default status to another account\n const isChangingDefault = formData.isDefault && !billingAccount?.isDefault;\n const needsDefaultConfirmation =\n isChangingDefault && currentDefaultAccount && currentDefaultAccount.id !== billingAccount?.id;\n\n // Perform the actual save operation\n const performSave = useCallback(async () => {\n if (!billingAccount?.id) return;\n\n setIsSaving(true);\n setSaveError(null);\n setSaveSuccess(false);\n setIsSetDefaultModalOpen(false);\n setPendingSave(false);\n\n try {\n // Update billing account details\n await updateBillingAccountMutation({\n variables: {\n input: {\n id: billingAccount.id,\n name: formData.name,\n email: formData.email,\n billingEmail: formData.billingEmail || null,\n currency: formData.currency,\n taxId: formData.taxId || null,\n },\n },\n });\n emit('billing.billing_account.updated', {\n route: '/billing/settings',\n entityId: billingAccount.id,\n source: 'settings',\n });\n\n // If setting as default, call the separate mutation\n if (isChangingDefault) {\n await setDefaultBillingAccountMutation({\n variables: { id: billingAccount.id },\n refetchQueries: ['GetBillingAccountsByTenant', 'GetDefaultBillingAccount'],\n });\n emit('billing.billing_account.updated', {\n route: '/billing/settings',\n entityId: billingAccount.id,\n source: 'settings.default',\n });\n }\n\n // Refetch to update cache\n if (selectedBillingAccountId) {\n await refetchSelected();\n } else {\n await refetchDefault();\n }\n\n setSaveSuccess(true);\n // Clear success message after 3 seconds\n setTimeout(() => setSaveSuccess(false), 3000);\n } catch (error) {\n setSaveError(error instanceof Error ? error.message : 'Failed to save changes');\n } finally {\n setIsSaving(false);\n }\n }, [\n billingAccount?.id,\n formData,\n isChangingDefault,\n updateBillingAccountMutation,\n setDefaultBillingAccountMutation,\n selectedBillingAccountId,\n refetchSelected,\n refetchDefault,\n emit,\n ]);\n\n // Handle save button click\n const handleSave = useCallback(() => {\n if (!billingAccount?.id) return;\n\n // If changing default and there's an existing default, show confirmation\n if (needsDefaultConfirmation) {\n setPendingSave(true);\n setIsSetDefaultModalOpen(true);\n } else {\n performSave();\n }\n }, [billingAccount?.id, needsDefaultConfirmation, performSave]);\n\n const replacementDefaultAccount = useMemo(\n () =>\n billingAccount\n ? billingAccounts.find((account) => account.id !== billingAccount.id && account.isActive)\n : undefined,\n [billingAccount, billingAccounts]\n );\n\n const archiveConfirmMessage = billingAccount\n ? billingAccount.isDefault\n ? `Archive ${billingAccount.name}? ${replacementDefaultAccount ? `${replacementDefaultAccount.name} will become the default account.` : 'No replacement default account is available.'}`\n : `Archive ${billingAccount.name}?`\n : '';\n\n const handleArchiveAccount = useCallback(async () => {\n if (!billingAccount?.id || !billingAccount.isActive) {\n return;\n }\n\n setIsArchiving(true);\n setSaveError(null);\n setSaveSuccess(false);\n\n try {\n await directBillingGraphqlRequest<\n { archiveBillingAccount: { id: string; isActive: boolean; isDefault: boolean } },\n { id: string; reason: string; replacementDefaultBillingAccountId: string | null }\n >({\n apiGatewayUrl,\n authToken,\n orgId,\n query: ArchiveBillingAccountSettingsDocument,\n variables: {\n id: billingAccount.id,\n reason: 'Archived from billing settings',\n replacementDefaultBillingAccountId: billingAccount.isDefault\n ? (replacementDefaultAccount?.id ?? null)\n : null,\n },\n });\n emit('billing.billing_account.archived', {\n route: '/billing/settings',\n entityId: billingAccount.id,\n source: 'settings',\n });\n\n await Promise.all([refetchAccounts(), refetchDefault()]);\n setSelectedBillingAccountId(replacementDefaultAccount?.id);\n setSaveSuccess(true);\n setTimeout(() => setSaveSuccess(false), 3000);\n } catch (error) {\n setSaveError(error instanceof Error ? error.message : 'Failed to archive billing account');\n } finally {\n setIsArchiving(false);\n }\n }, [\n apiGatewayUrl,\n authToken,\n billingAccount,\n replacementDefaultAccount,\n orgId,\n refetchAccounts,\n refetchDefault,\n emit,\n ]);\n\n if (!permissions.canManageBillingAccount) {\n return <AccessDenied message=\"You don't have permission to manage billing settings.\" />;\n }\n\n if (isLoading && !billingAccount) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-10 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={tr('billing.settings.billingSettings', 'Billing Settings')}\n description={tr(\n 'billing.settings.description',\n 'Manage your billing account configuration'\n )}\n />\n\n <PagePurpose>\n {tr(\n 'billing.settings.purpose',\n \"Configure how a billing account behaves — its name and contact details, default currency, tax information and whether it's your default account. Changes here affect how invoices are generated and which account new purchases bill to.\"\n )}\n </PagePurpose>\n\n {/* Account Selector (if multiple accounts) */}\n {billingAccounts.length > 1 && (\n <div className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setIsAccountSelectorOpen(!isAccountSelectorOpen)}\n className=\"w-full sm:w-auto flex items-center justify-between gap-3 px-4 py-3 border border-border-subtle rounded-lg bg-bg-surface hover:bg-bg-sunken/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 rounded-lg bg-action-primary-bg/10 flex items-center justify-center\">\n <svg\n className=\"size-4 text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n <div className=\"text-left\">\n <p className=\"text-sm font-medium text-text-primary\">\n {billingAccount?.name || tr('billing.settings.selectAccount', 'Select Account')}\n </p>\n <p className=\"text-xs text-text-secondary\">{billingAccount?.email}</p>\n </div>\n </div>\n <svg\n className={`size-4 text-text-secondary transition-transform ${isAccountSelectorOpen ? 'rotate-180' : ''}`}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </button>\n\n {isAccountSelectorOpen && (\n <>\n <div className=\"fixed inset-0 z-10\" onClick={() => setIsAccountSelectorOpen(false)} />\n <div className=\"absolute top-full left-0 right-0 sm:right-auto mt-1 w-full sm:w-80 bg-popover border border-border-subtle rounded-lg shadow-lg z-20 py-1 max-h-60 overflow-y-auto\">\n {billingAccounts.map((account) => (\n <button\n type=\"button\"\n key={account.id}\n onClick={() => handleSelectAccount(account)}\n className=\"w-full flex items-center justify-between gap-3 px-4 py-3 hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"size-8 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <svg\n className=\"size-4 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n <div className=\"text-left min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {account.name}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{account.email}</p>\n </div>\n </div>\n {billingAccount?.id === account.id && (\n <svg\n className=\"size-4 text-primary flex-shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </button>\n ))}\n </div>\n </>\n )}\n </div>\n )}\n\n {/* Account Settings */}\n <div className=\"border border-border-subtle rounded-lg bg-bg-surface\">\n <div className=\"p-6 border-b border-border-subtle\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.settings.accountInfo', 'Account Information')}\n </h2>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr(\n 'billing.settings.accountInfoDescription',\n 'Update your billing contact details and tax information'\n )}\n </p>\n </div>\n {billingAccount?.isDefault && (\n <span className=\"inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-action-primary-bg/10 text-primary rounded-full\">\n <svg className=\"size-3\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path\n fillRule=\"evenodd\"\n d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n clipRule=\"evenodd\"\n />\n </svg>\n {tr('billing.settings.defaultAccount', 'Default Account')}\n </span>\n )}\n </div>\n </div>\n <div className=\"p-6 space-y-6\">\n {/* Account Status Summary */}\n {billingAccount && (\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm pb-4 border-b border-border-subtle\">\n <div>\n <p className=\"text-text-secondary\">{tr('billing.settings.status', 'Status')}</p>\n <p\n className={`font-medium ${billingAccount.isActive ? 'text-status-success-text' : 'text-status-error-text'}`}\n >\n {billingAccount.isActive\n ? tr('billing.settings.active', 'Active')\n : tr('billing.settings.inactive', 'Inactive')}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">\n {tr('billing.settings.creditBalance', 'Credit Balance')}\n </p>\n <p className=\"font-medium text-text-primary\">\n {(billingAccount.creditAmount || 0).toLocaleString()} Credits\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">\n {tr('billing.settings.accountId', 'Account ID')}\n </p>\n <p\n className=\"font-mono text-xs text-text-primary truncate\"\n title={billingAccount.id}\n >\n {billingAccount.id}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">{tr('billing.settings.created', 'Created')}</p>\n <p className=\"font-medium text-text-primary\">\n {billingAccount.createdAt\n ? new Date(billingAccount.createdAt).toLocaleDateString()\n : tr('billing.settings.notAvailable', 'N/A')}\n </p>\n </div>\n </div>\n )}\n\n {/* Contact Details Section — Required fields above-the-fold */}\n <div>\n <h3 className=\"text-sm font-medium text-text-primary mb-4\">\n {tr('billing.settings.contactDetails', 'Contact Details')}\n </h3>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"billing-account-name\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.accountName', 'Account Name')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"billing-account-name\"\n type=\"text\"\n value={formData.name}\n onChange={(e) => handleInputChange('name', e.target.value)}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n </div>\n <div>\n <label\n htmlFor=\"billing-account-email\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.email', 'Email')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"billing-account-email\"\n type=\"email\"\n value={formData.email}\n onChange={(e) => handleInputChange('email', e.target.value)}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n </div>\n </div>\n </div>\n\n {/* Advanced billing settings — currency / billing email / tax ID. Most accounts keep defaults. */}\n <OptionalFields\n title={tr('billing.settings.advancedTitle', 'Advanced settings')}\n description={tr(\n 'billing.settings.advancedDescription',\n 'Billing email override, currency, and tax identification.'\n )}\n >\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"billing-account-billing-email\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.billingEmail', 'Billing Email')}\n </label>\n <input\n id=\"billing-account-billing-email\"\n type=\"email\"\n value={formData.billingEmail}\n onChange={(e) => handleInputChange('billingEmail', e.target.value)}\n placeholder={tr(\n 'billing.settings.billingEmailPlaceholder',\n 'Optional - defaults to account email'\n )}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'billing.settings.billingEmailHelp',\n 'Invoices will be sent to this email if provided'\n )}\n </p>\n </div>\n <div>\n <label\n htmlFor=\"billing-account-currency\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.currency', 'Currency')}\n </label>\n <select\n id=\"billing-account-currency\"\n value={formData.currency}\n onChange={(e) => handleInputChange('currency', e.target.value)}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n >\n <option value=\"USD\">USD - US Dollar</option>\n <option value=\"EUR\">EUR - Euro</option>\n <option value=\"GBP\">GBP - British Pound</option>\n <option value=\"INR\">INR - Indian Rupee</option>\n </select>\n </div>\n <div className=\"sm:col-span-2 max-w-md\">\n <label\n htmlFor=\"billing-account-tax-id\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.taxId', 'Tax ID / VAT Number')}\n </label>\n <input\n id=\"billing-account-tax-id\"\n type=\"text\"\n value={formData.taxId}\n onChange={(e) => handleInputChange('taxId', e.target.value)}\n placeholder={tr(\n 'billing.settings.taxIdPlaceholder',\n 'Enter your tax identification number'\n )}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'billing.settings.taxIdHelp',\n 'Used for invoices and tax compliance (e.g., GST, VAT number)'\n )}\n </p>\n </div>\n </div>\n </OptionalFields>\n\n {/* Default Account Setting */}\n {billingAccount?.isActive && billingAccounts.length > 1 && (\n <div className=\"pt-4 border-t border-border-subtle\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex items-center h-6\">\n <input\n type=\"checkbox\"\n id=\"isDefault\"\n checked={formData.isDefault}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, isDefault: e.target.checked }))\n }\n disabled={billingAccount?.isDefault}\n className=\"size-4 rounded border-border-subtle text-primary focus:ring-primary focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n </div>\n <div className=\"flex-1\">\n <label\n htmlFor=\"isDefault\"\n className=\"text-sm font-medium text-text-primary cursor-pointer\"\n >\n {tr('billing.settings.setDefault', 'Set as default billing account')}\n </label>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {billingAccount?.isDefault\n ? tr(\n 'billing.settings.currentDefaultDescription',\n 'This is currently your default billing account'\n )\n : tr(\n 'billing.settings.makePrimaryDescription',\n 'Make this your primary billing account for new subscriptions'\n )}\n </p>\n </div>\n </div>\n </div>\n )}\n\n {/* Save Button and Status */}\n <div className=\"pt-4 border-t border-border-subtle flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n {saveSuccess && (\n <div className=\"flex items-center gap-2 text-sm text-status-success-text\">\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=\"M5 13l4 4L19 7\"\n />\n </svg>\n {tr('billing.settings.saveSuccess', 'Changes saved successfully')}\n </div>\n )}\n {saveError && (\n <div className=\"flex items-center gap-2 text-sm text-status-error-text\">\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 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 {saveError}\n </div>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n {permissions.canArchiveBillingAccount && billingAccount?.isActive ? (\n <button\n type=\"button\"\n onClick={() => setIsArchiveConfirmOpen(true)}\n disabled={isSaving || isArchiving}\n className=\"px-4 py-2 text-sm font-medium border border-border-subtle text-status-error-text rounded-md hover:bg-status-error-bg-subtle disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isArchiving ? 'Archiving...' : 'Archive Account'}\n </button>\n ) : null}\n <button\n type=\"button\"\n onClick={handleSave}\n disabled={\n !hasChanges || isSaving || isArchiving || !formData.name || !formData.email\n }\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2\"\n >\n {isSaving && (\n <svg className=\"size-4 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n )}\n {isSaving ? 'Saving...' : 'Save Changes'}\n </button>\n </div>\n </div>\n </div>\n </div>\n\n {/* Set as Default Confirmation Modal */}\n {isSetDefaultModalOpen && pendingSave && (\n <>\n <div\n className=\"fixed inset-0 bg-overlay-scrim z-40\"\n onClick={() => {\n if (!isSaving) {\n setIsSetDefaultModalOpen(false);\n setPendingSave(false);\n // Reset the checkbox if cancelled\n setFormData((prev) => ({ ...prev, isDefault: billingAccount?.isDefault || false }));\n }\n }}\n />\n <div className=\"fixed inset-0 flex items-center justify-center z-50 p-4\">\n <div className=\"bg-bg-surface border border-border-subtle rounded-lg shadow-xl max-w-md w-full p-6\">\n <h3 className=\"text-lg font-semibold text-text-primary mb-2\">\n Confirm Default Account Change\n </h3>\n <p className=\"text-sm text-text-secondary mb-4\">\n You are about to set{' '}\n <strong className=\"text-text-primary\">{billingAccount?.name}</strong> as your\n default billing account and save your changes.\n </p>\n\n {currentDefaultAccount && currentDefaultAccount.id !== billingAccount?.id && (\n <div className=\"mb-4 p-3 bg-status-warning-bg-subtle border border-border-subtle rounded-md\">\n <div className=\"flex items-start gap-2\">\n <svg\n className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"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 className=\"text-sm\">\n <p className=\"font-medium text-status-warning-text\">\n Current default will change\n </p>\n <p className=\"text-status-warning-text mt-0.5\">\n <strong>{currentDefaultAccount.name}</strong> will no longer be the default\n account.\n </p>\n </div>\n </div>\n </div>\n )}\n\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => {\n setIsSetDefaultModalOpen(false);\n setPendingSave(false);\n // Reset the checkbox if cancelled\n setFormData((prev) => ({\n ...prev,\n isDefault: billingAccount?.isDefault || false,\n }));\n }}\n disabled={isSaving}\n className=\"px-4 py-2 text-sm font-medium text-text-primary border border-border-subtle rounded-md hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={performSave}\n disabled={isSaving}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 disabled:opacity-50 transition-colors flex items-center gap-2\"\n >\n {isSaving && (\n <svg className=\"size-4 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n )}\n {isSaving ? 'Saving...' : 'Save Changes'}\n </button>\n </div>\n </div>\n </div>\n </>\n )}\n\n <AlertDialog open={isArchiveConfirmOpen} onOpenChange={setIsArchiveConfirmOpen}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Archive billing account?</AlertDialogTitle>\n <AlertDialogDescription>{archiveConfirmMessage}</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={() => {\n setIsArchiveConfirmOpen(false);\n void handleArchiveAccount();\n }}\n >\n Archive Account\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SettingsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/SettingsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Settings Page\n * Billing account settings and configuration\n */\n\nimport { useState, useMemo, useEffect, useCallback, type FC } from 'react';\nimport { useBillingEventEmitter } from '../../../hooks/useBillingEventEmitter';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n OptionalFields,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\nimport {\n useGetBillingAccountSettingsQuery,\n useGetBillingAccountsSettingsQuery,\n useGetDefaultBillingAccountSettingsQuery,\n useUpdateBillingAccountSettingsMutation,\n useSetDefaultBillingAccountSettingsMutation,\n ArchiveBillingAccountSettingsDocument,\n} from '../../../../generated/global-operations';\nimport type { BillingAccount } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n// Helper to get billing account ID from URL (for navigation from Overview page)\nconst getInitialBillingAccountId = (): string | undefined => {\n if (typeof window === 'undefined') return undefined;\n const urlParams = new URLSearchParams(window.location.search);\n return urlParams.get('billingAccountId') || undefined;\n};\n\nexport const SettingsPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const permissions = useBillingPermissions();\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n const { emit } = useBillingEventEmitter();\n\n // State - initialize with URL param if present\n const [selectedBillingAccountId, setSelectedBillingAccountId] = useState<string | undefined>(\n getInitialBillingAccountId\n );\n const [isAccountSelectorOpen, setIsAccountSelectorOpen] = useState(false);\n const [isSaving, setIsSaving] = useState(false);\n const [isArchiving, setIsArchiving] = useState(false);\n const [isArchiveConfirmOpen, setIsArchiveConfirmOpen] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n const [saveSuccess, setSaveSuccess] = useState(false);\n\n // Set default confirmation modal state\n const [isSetDefaultModalOpen, setIsSetDefaultModalOpen] = useState(false);\n const [pendingSave, setPendingSave] = useState(false);\n\n // Form state - includes isDefault toggle\n const [formData, setFormData] = useState({\n name: '',\n email: '',\n billingEmail: '',\n currency: 'USD',\n taxId: '',\n isDefault: false,\n });\n\n // Fetch all billing accounts for the selector (lightweight query without subscriptions)\n const {\n data: accountsData,\n loading: isLoadingAccounts,\n refetch: refetchAccounts,\n } = useGetBillingAccountsSettingsQuery({});\n const billingAccounts = useMemo<BillingAccount[]>(\n () => accountsData?.getBillingAccountsByOrg || [],\n [accountsData?.getBillingAccountsByOrg]\n );\n\n // Fetch default billing account (without subscriptions)\n const {\n data: defaultData,\n loading: isLoadingDefault,\n refetch: refetchDefault,\n } = useGetDefaultBillingAccountSettingsQuery({});\n const defaultBillingAccount: BillingAccount | null =\n defaultData?.getDefaultBillingAccount || null;\n\n // Always fetch selected billing account if we have an ID from URL (without subscriptions)\n const {\n data: selectedData,\n loading: isLoadingSelected,\n refetch: refetchSelected,\n } = useGetBillingAccountSettingsQuery({\n variables: { id: selectedBillingAccountId! },\n skip: !selectedBillingAccountId,\n });\n const selectedAccountData: BillingAccount | null = selectedData?.getBillingAccount || null;\n\n // Mutation for updating billing account\n const [updateBillingAccountMutation] = useUpdateBillingAccountSettingsMutation();\n\n // Mutation for setting default billing account\n const [setDefaultBillingAccountMutation] = useSetDefaultBillingAccountSettingsMutation();\n\n // Determine which account to use - prioritize URL param\n const billingAccount = useMemo(() => {\n // If we have a selected account ID from URL\n if (selectedBillingAccountId) {\n // First try to use the fetched selected account data\n if (selectedAccountData) {\n return selectedAccountData;\n }\n // While loading, try to find from the accounts list\n const fromList = billingAccounts.find((a) => a.id === selectedBillingAccountId);\n if (fromList) {\n return fromList;\n }\n // If still loading and not in list, return null to show loading state\n if (isLoadingSelected) {\n return null;\n }\n }\n // No URL param - use default account\n return defaultBillingAccount;\n }, [\n selectedBillingAccountId,\n selectedAccountData,\n billingAccounts,\n isLoadingSelected,\n defaultBillingAccount,\n ]);\n\n const isLoading =\n isLoadingAccounts || isLoadingDefault || (selectedBillingAccountId && isLoadingSelected);\n\n // Update form when billing account changes\n useEffect(() => {\n if (billingAccount) {\n setFormData({\n name: billingAccount.name || '',\n email: billingAccount.email || '',\n billingEmail: billingAccount.billingEmail || '',\n currency: billingAccount.currency || 'USD',\n taxId: billingAccount.taxId || '',\n isDefault: billingAccount.isDefault || false,\n });\n }\n }, [billingAccount]);\n\n // Handle account selection\n const handleSelectAccount = (account: BillingAccount) => {\n setSelectedBillingAccountId(account.id);\n setIsAccountSelectorOpen(false);\n setSaveSuccess(false);\n setSaveError(null);\n };\n\n // Handle form input changes\n const handleInputChange = (field: keyof typeof formData, value: string) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n setSaveSuccess(false);\n setSaveError(null);\n };\n\n // Find the current default account\n const currentDefaultAccount = useMemo(() => {\n return billingAccounts.find((a) => a.isDefault) || null;\n }, [billingAccounts]);\n\n // Check if form has changes\n const hasChanges = useMemo(() => {\n if (!billingAccount) return false;\n return (\n formData.name !== (billingAccount.name || '') ||\n formData.email !== (billingAccount.email || '') ||\n formData.billingEmail !== (billingAccount.billingEmail || '') ||\n formData.currency !== (billingAccount.currency || 'USD') ||\n formData.taxId !== (billingAccount.taxId || '') ||\n formData.isDefault !== (billingAccount.isDefault || false)\n );\n }, [formData, billingAccount]);\n\n // Check if trying to change default status to another account\n const isChangingDefault = formData.isDefault && !billingAccount?.isDefault;\n const needsDefaultConfirmation =\n isChangingDefault && currentDefaultAccount && currentDefaultAccount.id !== billingAccount?.id;\n\n // Perform the actual save operation\n const performSave = useCallback(async () => {\n if (!billingAccount?.id) return;\n\n setIsSaving(true);\n setSaveError(null);\n setSaveSuccess(false);\n setIsSetDefaultModalOpen(false);\n setPendingSave(false);\n\n try {\n // Update billing account details\n await updateBillingAccountMutation({\n variables: {\n input: {\n id: billingAccount.id,\n name: formData.name,\n email: formData.email,\n billingEmail: formData.billingEmail || null,\n currency: formData.currency,\n taxId: formData.taxId || null,\n },\n },\n });\n emit('billing.billing_account.updated', {\n route: '/billing/settings',\n entityId: billingAccount.id,\n source: 'settings',\n });\n\n // If setting as default, call the separate mutation\n if (isChangingDefault) {\n await setDefaultBillingAccountMutation({\n variables: { id: billingAccount.id },\n refetchQueries: ['GetBillingAccountsByTenant', 'GetDefaultBillingAccount'],\n });\n emit('billing.billing_account.updated', {\n route: '/billing/settings',\n entityId: billingAccount.id,\n source: 'settings.default',\n });\n }\n\n // Refetch to update cache\n if (selectedBillingAccountId) {\n await refetchSelected();\n } else {\n await refetchDefault();\n }\n\n setSaveSuccess(true);\n // Clear success message after 3 seconds\n setTimeout(() => setSaveSuccess(false), 3000);\n } catch (error) {\n setSaveError(error instanceof Error ? error.message : 'Failed to save changes');\n } finally {\n setIsSaving(false);\n }\n }, [\n billingAccount?.id,\n formData,\n isChangingDefault,\n updateBillingAccountMutation,\n setDefaultBillingAccountMutation,\n selectedBillingAccountId,\n refetchSelected,\n refetchDefault,\n emit,\n ]);\n\n // Handle save button click\n const handleSave = useCallback(() => {\n if (!billingAccount?.id) return;\n\n // If changing default and there's an existing default, show confirmation\n if (needsDefaultConfirmation) {\n setPendingSave(true);\n setIsSetDefaultModalOpen(true);\n } else {\n performSave();\n }\n }, [billingAccount?.id, needsDefaultConfirmation, performSave]);\n\n const replacementDefaultAccount = useMemo(\n () =>\n billingAccount\n ? billingAccounts.find((account) => account.id !== billingAccount.id && account.isActive)\n : undefined,\n [billingAccount, billingAccounts]\n );\n\n const archiveConfirmMessage = billingAccount\n ? billingAccount.isDefault\n ? `Archive ${billingAccount.name}? ${replacementDefaultAccount ? `${replacementDefaultAccount.name} will become the default account.` : 'No replacement default account is available.'}`\n : `Archive ${billingAccount.name}?`\n : '';\n\n const handleArchiveAccount = useCallback(async () => {\n if (!billingAccount?.id || !billingAccount.isActive) {\n return;\n }\n\n setIsArchiving(true);\n setSaveError(null);\n setSaveSuccess(false);\n\n try {\n await directBillingGraphqlRequest<\n { archiveBillingAccount: { id: string; isActive: boolean; isDefault: boolean } },\n { id: string; reason: string; replacementDefaultBillingAccountId: string | null }\n >({\n apiGatewayUrl,\n authToken,\n orgId,\n query: ArchiveBillingAccountSettingsDocument,\n variables: {\n id: billingAccount.id,\n reason: 'Archived from billing settings',\n replacementDefaultBillingAccountId: billingAccount.isDefault\n ? (replacementDefaultAccount?.id ?? null)\n : null,\n },\n });\n emit('billing.billing_account.archived', {\n route: '/billing/settings',\n entityId: billingAccount.id,\n source: 'settings',\n });\n\n await Promise.all([refetchAccounts(), refetchDefault()]);\n setSelectedBillingAccountId(replacementDefaultAccount?.id);\n setSaveSuccess(true);\n setTimeout(() => setSaveSuccess(false), 3000);\n } catch (error) {\n setSaveError(error instanceof Error ? error.message : 'Failed to archive billing account');\n } finally {\n setIsArchiving(false);\n }\n }, [\n apiGatewayUrl,\n authToken,\n billingAccount,\n replacementDefaultAccount,\n orgId,\n refetchAccounts,\n refetchDefault,\n emit,\n ]);\n\n if (!permissions.canManageBillingAccount) {\n return <AccessDenied message=\"You don't have permission to manage billing settings.\" />;\n }\n\n if (isLoading && !billingAccount) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-10 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={tr('billing.settings.billingSettings', 'Billing Settings')}\n description={tr(\n 'billing.settings.description',\n 'Manage your billing account configuration'\n )}\n />\n\n <PagePurpose>\n {tr(\n 'billing.settings.purpose',\n \"Configure how a billing account behaves — its name and contact details, default currency, tax information and whether it's your default account. Changes here affect how invoices are generated and which account new purchases bill to.\"\n )}\n </PagePurpose>\n\n {/* Account Selector (if multiple accounts) */}\n {billingAccounts.length > 1 && (\n <div className=\"relative\">\n <button\n type=\"button\"\n onClick={() => setIsAccountSelectorOpen(!isAccountSelectorOpen)}\n className=\"w-full sm:w-auto flex items-center justify-between gap-3 px-4 py-3 border border-border-subtle rounded-lg bg-bg-surface hover:bg-bg-sunken/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 rounded-lg bg-action-primary-bg/10 flex items-center justify-center\">\n <svg\n className=\"size-4 text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n <div className=\"text-left\">\n <p className=\"text-sm font-medium text-text-primary\">\n {billingAccount?.name || tr('billing.settings.selectAccount', 'Select Account')}\n </p>\n <p className=\"text-xs text-text-secondary\">{billingAccount?.email}</p>\n </div>\n </div>\n <svg\n className={`size-4 text-text-secondary transition-transform ${isAccountSelectorOpen ? 'rotate-180' : ''}`}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </button>\n\n {isAccountSelectorOpen && (\n <>\n <div className=\"fixed inset-0 z-10\" onClick={() => setIsAccountSelectorOpen(false)} />\n <div className=\"absolute top-full left-0 right-0 sm:right-auto mt-1 w-full sm:w-80 bg-popover border border-border-subtle rounded-lg shadow-lg z-20 py-1 max-h-60 overflow-y-auto\">\n {billingAccounts.map((account) => (\n <button\n type=\"button\"\n key={account.id}\n onClick={() => handleSelectAccount(account)}\n className=\"w-full flex items-center justify-between gap-3 px-4 py-3 hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"size-8 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <svg\n className=\"size-4 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n <div className=\"text-left min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {account.name}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{account.email}</p>\n </div>\n </div>\n {billingAccount?.id === account.id && (\n <svg\n className=\"size-4 text-primary flex-shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </button>\n ))}\n </div>\n </>\n )}\n </div>\n )}\n\n {/* Account Settings */}\n <div className=\"border border-border-subtle rounded-lg bg-bg-surface\">\n <div className=\"p-6 border-b border-border-subtle\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.settings.accountInfo', 'Account Information')}\n </h2>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr(\n 'billing.settings.accountInfoDescription',\n 'Update your billing contact details and tax information'\n )}\n </p>\n </div>\n {billingAccount?.isDefault && (\n <span className=\"inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-action-primary-bg/10 text-primary rounded-full\">\n <svg className=\"size-3\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path\n fillRule=\"evenodd\"\n d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n clipRule=\"evenodd\"\n />\n </svg>\n {tr('billing.settings.defaultAccount', 'Default Account')}\n </span>\n )}\n </div>\n </div>\n <div className=\"p-6 space-y-6\">\n {/* Account Status Summary */}\n {billingAccount && (\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm pb-4 border-b border-border-subtle\">\n <div>\n <p className=\"text-text-secondary\">{tr('billing.settings.status', 'Status')}</p>\n <p\n className={`font-medium ${billingAccount.isActive ? 'text-status-success-text' : 'text-status-error-text'}`}\n >\n {billingAccount.isActive\n ? tr('billing.settings.active', 'Active')\n : tr('billing.settings.inactive', 'Inactive')}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">\n {tr('billing.settings.creditBalance', 'Credit Balance')}\n </p>\n <p className=\"font-medium text-text-primary\">\n {(billingAccount.creditAmount || 0).toLocaleString()} Credits\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">\n {tr('billing.settings.accountId', 'Account ID')}\n </p>\n <p\n className=\"font-mono text-xs text-text-primary truncate\"\n title={billingAccount.id}\n >\n {billingAccount.id}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">{tr('billing.settings.created', 'Created')}</p>\n <p className=\"font-medium text-text-primary\">\n {billingAccount.createdAt\n ? new Date(billingAccount.createdAt).toLocaleDateString()\n : tr('billing.settings.notAvailable', 'N/A')}\n </p>\n </div>\n </div>\n )}\n\n {/* Contact Details Section — Required fields above-the-fold */}\n <div>\n <h3 className=\"text-sm font-medium text-text-primary mb-4\">\n {tr('billing.settings.contactDetails', 'Contact Details')}\n </h3>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"billing-account-name\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.accountName', 'Account Name')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"billing-account-name\"\n type=\"text\"\n value={formData.name}\n onChange={(e) => handleInputChange('name', e.target.value)}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n </div>\n <div>\n <label\n htmlFor=\"billing-account-email\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.email', 'Email')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"billing-account-email\"\n type=\"email\"\n value={formData.email}\n onChange={(e) => handleInputChange('email', e.target.value)}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n </div>\n </div>\n </div>\n\n {/* Advanced billing settings — currency / billing email / tax ID. Most accounts keep defaults. */}\n <OptionalFields\n title={tr('billing.settings.advancedTitle', 'Advanced settings')}\n description={tr(\n 'billing.settings.advancedDescription',\n 'Billing email override, currency, and tax identification.'\n )}\n >\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"billing-account-billing-email\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.billingEmail', 'Billing Email')}\n </label>\n <input\n id=\"billing-account-billing-email\"\n type=\"email\"\n value={formData.billingEmail}\n onChange={(e) => handleInputChange('billingEmail', e.target.value)}\n placeholder={tr(\n 'billing.settings.billingEmailPlaceholder',\n 'Optional - defaults to account email'\n )}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'billing.settings.billingEmailHelp',\n 'Invoices will be sent to this email if provided'\n )}\n </p>\n </div>\n <div>\n <label\n htmlFor=\"billing-account-currency\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.currency', 'Currency')}\n </label>\n <select\n id=\"billing-account-currency\"\n value={formData.currency}\n onChange={(e) => handleInputChange('currency', e.target.value)}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n >\n <option value=\"USD\">USD - US Dollar</option>\n <option value=\"EUR\">EUR - Euro</option>\n <option value=\"GBP\">GBP - British Pound</option>\n <option value=\"INR\">INR - Indian Rupee</option>\n </select>\n </div>\n <div className=\"sm:col-span-2 max-w-md\">\n <label\n htmlFor=\"billing-account-tax-id\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n {tr('billing.settings.taxId', 'Tax ID / VAT Number')}\n </label>\n <input\n id=\"billing-account-tax-id\"\n type=\"text\"\n value={formData.taxId}\n onChange={(e) => handleInputChange('taxId', e.target.value)}\n placeholder={tr(\n 'billing.settings.taxIdPlaceholder',\n 'Enter your tax identification number'\n )}\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-md bg-bg-surface text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'billing.settings.taxIdHelp',\n 'Used for invoices and tax compliance (e.g., GST, VAT number)'\n )}\n </p>\n </div>\n </div>\n </OptionalFields>\n\n {/* Default Account Setting */}\n {billingAccount?.isActive && billingAccounts.length > 1 && (\n <div className=\"pt-4 border-t border-border-subtle\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex items-center h-6\">\n <input\n type=\"checkbox\"\n id=\"isDefault\"\n checked={formData.isDefault}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, isDefault: e.target.checked }))\n }\n disabled={billingAccount?.isDefault}\n className=\"size-4 rounded border-border-subtle text-primary focus:ring-primary focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n </div>\n <div className=\"flex-1\">\n <label\n htmlFor=\"isDefault\"\n className=\"text-sm font-medium text-text-primary cursor-pointer\"\n >\n {tr('billing.settings.setDefault', 'Set as default billing account')}\n </label>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {billingAccount?.isDefault\n ? tr(\n 'billing.settings.currentDefaultDescription',\n 'This is currently your default billing account'\n )\n : tr(\n 'billing.settings.makePrimaryDescription',\n 'Make this your primary billing account for new subscriptions'\n )}\n </p>\n </div>\n </div>\n </div>\n )}\n\n {/* Save Button and Status */}\n <div className=\"pt-4 border-t border-border-subtle flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n {saveSuccess && (\n <div className=\"flex items-center gap-2 text-sm text-status-success-text\">\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=\"M5 13l4 4L19 7\"\n />\n </svg>\n {tr('billing.settings.saveSuccess', 'Changes saved successfully')}\n </div>\n )}\n {saveError && (\n <div className=\"flex items-center gap-2 text-sm text-status-error-text\">\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 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 {saveError}\n </div>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n {permissions.canArchiveBillingAccount && billingAccount?.isActive ? (\n <button\n type=\"button\"\n onClick={() => setIsArchiveConfirmOpen(true)}\n disabled={isSaving || isArchiving}\n className=\"px-4 py-2 text-sm font-medium border border-border-subtle text-status-error-text rounded-md hover:bg-status-error-bg-subtle disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isArchiving ? 'Archiving...' : 'Archive Account'}\n </button>\n ) : null}\n <button\n type=\"button\"\n onClick={handleSave}\n disabled={\n !hasChanges || isSaving || isArchiving || !formData.name || !formData.email\n }\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2\"\n >\n {isSaving && (\n <svg className=\"size-4 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n )}\n {isSaving ? 'Saving...' : 'Save Changes'}\n </button>\n </div>\n </div>\n </div>\n </div>\n\n {/* Set as Default Confirmation Modal */}\n {isSetDefaultModalOpen && pendingSave && (\n <>\n <div\n className=\"fixed inset-0 bg-overlay-scrim z-40\"\n onClick={() => {\n if (!isSaving) {\n setIsSetDefaultModalOpen(false);\n setPendingSave(false);\n // Reset the checkbox if cancelled\n setFormData((prev) => ({ ...prev, isDefault: billingAccount?.isDefault || false }));\n }\n }}\n />\n <div className=\"fixed inset-0 flex items-center justify-center z-50 p-4\">\n <div className=\"bg-bg-surface border border-border-subtle rounded-lg shadow-xl max-w-md w-full p-6\">\n <h3 className=\"text-lg font-semibold text-text-primary mb-2\">\n Confirm Default Account Change\n </h3>\n <p className=\"text-sm text-text-secondary mb-4\">\n You are about to set{' '}\n <strong className=\"text-text-primary\">{billingAccount?.name}</strong> as your\n default billing account and save your changes.\n </p>\n\n {currentDefaultAccount && currentDefaultAccount.id !== billingAccount?.id && (\n <div className=\"mb-4 p-3 bg-status-warning-bg-subtle border border-border-subtle rounded-md\">\n <div className=\"flex items-start gap-2\">\n <svg\n className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"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 className=\"text-sm\">\n <p className=\"font-medium text-status-warning-text\">\n Current default will change\n </p>\n <p className=\"text-status-warning-text mt-0.5\">\n <strong>{currentDefaultAccount.name}</strong> will no longer be the default\n account.\n </p>\n </div>\n </div>\n </div>\n )}\n\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => {\n setIsSetDefaultModalOpen(false);\n setPendingSave(false);\n // Reset the checkbox if cancelled\n setFormData((prev) => ({\n ...prev,\n isDefault: billingAccount?.isDefault || false,\n }));\n }}\n disabled={isSaving}\n className=\"px-4 py-2 text-sm font-medium text-text-primary border border-border-subtle rounded-md hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={performSave}\n disabled={isSaving}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 disabled:opacity-50 transition-colors flex items-center gap-2\"\n >\n {isSaving && (\n <svg className=\"size-4 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n )}\n {isSaving ? 'Saving...' : 'Save Changes'}\n </button>\n </div>\n </div>\n </div>\n </>\n )}\n\n <AlertDialog open={isArchiveConfirmOpen} onOpenChange={setIsArchiveConfirmOpen}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Archive billing account?</AlertDialogTitle>\n <AlertDialogDescription>{archiveConfirmMessage}</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={() => {\n setIsArchiveConfirmOpen(false);\n void handleArchiveAccount();\n }}\n >\n Archive Account\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamPermissionsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Team & Permissions Page\n *\n * Lets a Billing Admin manage billing-scope role assignments for a specific\n * billing account. Calls global-rbac-svc directly via the same gateway URL\n * used by the rest of the billing module.\n *\n * Scope: `scopeType: \"billing\"`, `scopeId: <billingAccountId>`\n * Roles cloned per billing account at creation time:\n * - Billing Admin (full access to this billing account)\n * - Billing Viewer (read-only access to this billing account)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n PagePurpose,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, ActorIdentity, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useBillingAccounts } from '../../dashboard/hooks/useDashboard';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\nimport { useActorProfiles } from '../../../shared/hooks';\n\n// Basic UUID-v1-through-v5 shape — accept any 8-4-4-4-12 hex pattern.\n// We intentionally don't strictly validate the version nibble; the rbac\n// service is the authoritative ID validator.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Role {\n id: string;\n name: string;\n description?: string | null;\n priority: number;\n isSystem: boolean;\n}\n\ninterface ActorRoleAssignment {\n id: string;\n actorId: string;\n actorType: string;\n roleId: string;\n scopeId: string;\n isActive: boolean;\n expiresAt?: string | null;\n role?: Role;\n}\n\ninterface ActorRbacSummary {\n actorId: string;\n actorType: string;\n roleAssignments: ActorRoleAssignment[];\n effectivePermissionCount?: number;\n}\n\nconst ROLES_QUERY = `\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n`;\n\nconst ACTORS_QUERY = `\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n`;\n\nconst ASSIGN_ROLE_MUTATION = `\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n`;\n\nconst REMOVE_ROLE_MUTATION = `\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n`;\n\nexport const TeamPermissionsPage: FC = () => {\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Resolution priority: URL `?billingAccountId=` → persisted localStorage →\n // default/first available account. Pass `billingAccounts` so first-time\n // users with empty localStorage still land on the default account\n // (without it the hook's auto-select effect can't fire and the page\n // dead-ends on \"No billing account selected\").\n const { billingAccounts } = useBillingAccounts();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n\n // Share the resolved account with `useBillingPermissions` so its\n // billing-scope permissions fetch observes the same ID. Without this,\n // a first-time user (no URL param, empty localStorage) would see the\n // page resolve to the default account here while the permissions hook\n // still sat at null — billing-scope-only admins would see AccessDenied\n // until a navigation/reload propagated the localStorage write.\n const permissions = useBillingPermissions({ billingAccountIdOverride: billingAccountId });\n\n const [roles, setRoles] = useState<Role[]>([]);\n const [actors, setActors] = useState<ActorRbacSummary[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Add-member form\n const [newActorId, setNewActorId] = useState('');\n const [newRoleId, setNewRoleId] = useState('');\n const [isAdding, setIsAdding] = useState(false);\n const [removeAssignmentId, setRemoveAssignmentId] = useState<string | null>(null);\n\n const gqlPost = useCallback(\n async <T,>(query: string, variables: Record<string, unknown>): Promise<T> => {\n return directBillingGraphqlRequest<T>({\n apiGatewayUrl,\n authToken,\n orgId,\n query,\n variables,\n });\n },\n [apiGatewayUrl, authToken, orgId]\n );\n\n const loadData = useCallback(async () => {\n if (!billingAccountId) return;\n setIsLoading(true);\n setError(null);\n try {\n const [rolesResp, actorsResp] = await Promise.all([\n gqlPost<{ roles: Role[] }>(ROLES_QUERY, { scopeId: billingAccountId }),\n gqlPost<{ actors: ActorRbacSummary[] }>(ACTORS_QUERY, {\n scopeId: billingAccountId,\n }),\n ]);\n setRoles(rolesResp.roles ?? []);\n setActors(actorsResp.actors ?? []);\n // Default the role-picker to \"Billing Viewer\" if available, else first role\n const viewer = (rolesResp.roles ?? []).find((r) => /viewer/i.test(r.name));\n setNewRoleId(viewer?.id ?? rolesResp.roles?.[0]?.id ?? '');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load team data');\n } finally {\n setIsLoading(false);\n }\n }, [billingAccountId, gqlPost]);\n\n useEffect(() => {\n void loadData();\n }, [loadData]);\n\n const sortedActors = useMemo(\n () =>\n [...actors].sort((a, b) => {\n const priA = Math.max(...a.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n const priB = Math.max(...b.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n return priB - priA;\n }),\n [actors]\n );\n\n const profileInputs = useMemo(\n () =>\n sortedActors.map((a) => ({\n actorId: a.actorId,\n actorType: a.actorType,\n })),\n [sortedActors]\n );\n const profileMap = useActorProfiles(profileInputs);\n\n const billingAccountName = useMemo(() => {\n if (!billingAccountId) return undefined;\n return billingAccounts.find((acct) => acct.id === billingAccountId)?.name ?? undefined;\n }, [billingAccountId, billingAccounts]);\n\n const handleAddMember = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!billingAccountId || !newActorId.trim() || !newRoleId) return;\n const trimmedActorId = newActorId.trim();\n if (!UUID_RE.test(trimmedActorId)) {\n setError('User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.');\n return;\n }\n setIsAdding(true);\n setError(null);\n try {\n await gqlPost(ASSIGN_ROLE_MUTATION, {\n input: {\n actorId: trimmedActorId,\n actorType: 'USER',\n roleId: newRoleId,\n scopeId: billingAccountId,\n },\n });\n setNewActorId('');\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to assign role');\n } finally {\n setIsAdding(false);\n }\n };\n\n const handleRemoveAssignment = async (assignmentId: string) => {\n setError(null);\n try {\n await gqlPost(REMOVE_ROLE_MUTATION, { assignmentId });\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove role');\n }\n };\n\n if (!permissions.canManageBillingAccount) {\n return (\n <AccessDenied message=\"You don't have permission to manage team and permissions for this billing account.\" />\n );\n }\n\n if (!billingAccountId) {\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Select a billing account to manage who can access it.\"\n />\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No billing account selected\"\n description=\"Pick one from the billing account selector to manage its members and roles.\"\n />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Control who can manage this billing account and what they can do.\"\n />\n\n <PagePurpose>\n Decide who on your team can see and manage this billing account. Assign the Billing Admin\n role (full access) or Billing Viewer (read-only) to specific people, and revoke access when\n someone no longer needs it.\n </PagePurpose>\n\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 text-sm\">\n <span className=\"font-medium text-text-primary\">Billing account:</span>{' '}\n <span className=\"text-text-primary\">{billingAccountName ?? 'Unnamed account'}</span>\n {billingAccountId ? (\n <details className=\"mt-2 text-xs text-text-secondary\">\n <summary className=\"cursor-pointer\">Account ID</summary>\n <span className=\"mt-1 inline-block font-mono\">{billingAccountId}</span>\n </details>\n ) : null}\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-status-error-border/30 bg-status-error-bg/5 p-4 text-sm text-status-error-text\">\n {error}\n </div>\n )}\n\n {/* Add member form */}\n <form\n onSubmit={(e) => void handleAddMember(e)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-text-primary\">Add member</h3>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-3\">\n <div className=\"md:col-span-2\">\n <label htmlFor=\"newActorId\" className=\"block text-xs font-medium text-text-secondary\">\n User ID\n </label>\n <input\n id=\"newActorId\"\n type=\"text\"\n value={newActorId}\n onChange={(e) => setNewActorId(e.target.value)}\n placeholder=\"Paste the user ID from their profile page\"\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n required\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n Find this on the user's profile page.\n </p>\n </div>\n <div>\n <label htmlFor=\"newRoleId\" className=\"block text-xs font-medium text-text-secondary\">\n Role\n </label>\n <select\n id=\"newRoleId\"\n value={newRoleId}\n onChange={(e) => setNewRoleId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n required\n >\n {roles.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={isAdding || !newActorId.trim() || !newRoleId}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text disabled:opacity-50\"\n >\n {isAdding ? 'Adding…' : 'Add member'}\n </button>\n </form>\n\n {/* Members table */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"border-b border-border-subtle px-4 py-3\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Members</h3>\n <p className=\"text-xs text-text-secondary\">\n Each member's role applies only to this billing account.\n </p>\n </div>\n {isLoading ? (\n <div className=\"p-6 text-sm text-text-secondary\">Loading…</div>\n ) : sortedActors.length === 0 ? (\n <div className=\"p-6 text-sm text-text-secondary\">No members assigned yet.</div>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle text-left text-xs text-text-secondary\">\n <th className=\"px-4 py-2\">Member</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Roles</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {sortedActors.map((actor) => (\n <tr key={actor.actorId} className=\"border-b border-border-subtle last:border-0\">\n <td className=\"px-4 py-3\">\n <ActorIdentity\n profile={\n profileMap[actor.actorId] ?? {\n actorId: actor.actorId,\n actorType: actor.actorType,\n displayName:\n actor.actorType.toLowerCase() === 'user'\n ? `User ${actor.actorId.slice(0, 8)}…`\n : actor.actorType,\n secondaryLabel: actor.actorId.slice(0, 12),\n resolved: false,\n }\n }\n variant=\"cell\"\n />\n </td>\n <td className=\"px-4 py-3 text-xs capitalize text-text-secondary\">\n {actor.actorType.toLowerCase()}\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {actor.roleAssignments\n .filter((ra) => ra.isActive)\n .map((ra) => (\n <span\n key={ra.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border-subtle bg-bg-sunken px-2 py-0.5 text-xs\"\n >\n {ra.role?.name ?? ra.roleId}\n <button\n type=\"button\"\n onClick={() => setRemoveAssignmentId(ra.id)}\n className=\"text-text-secondary hover:text-status-error-text\"\n aria-label={`Remove ${ra.role?.name ?? 'role'}`}\n >\n ×\n </button>\n </span>\n ))}\n </div>\n </td>\n <td className=\"px-4 py-3 text-right\">\n <span className=\"text-xs text-text-secondary\">\n {actor.effectivePermissionCount ?? '—'} permissions\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {/* Roles reference */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Available roles</h3>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Roles are cloned from the system templates when a billing account is created.\n </p>\n <ul className=\"mt-3 space-y-2\">\n {roles.map((r) => (\n <li\n key={r.id}\n className=\"flex items-start gap-3 rounded-md border border-border-subtle bg-bg-surface p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-text-primary\">{r.name}</div>\n {r.description && (\n <p className=\"mt-1 text-xs text-text-secondary\">{r.description}</p>\n )}\n </div>\n <span className=\"text-xs text-text-secondary\">priority {r.priority}</span>\n </li>\n ))}\n </ul>\n </div>\n\n <AlertDialog\n open={removeAssignmentId !== null}\n onOpenChange={(open) => {\n if (!open) setRemoveAssignmentId(null);\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Remove role assignment?</AlertDialogTitle>\n <AlertDialogDescription>\n Remove this role assignment? The member will lose the access this role grants for this\n billing account.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={() => {\n const id = removeAssignmentId;\n setRemoveAssignmentId(null);\n if (id) void handleRemoveAssignment(id);\n }}\n >\n Remove\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TeamPermissionsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Team & Permissions Page\n *\n * Lets a Billing Admin manage billing-scope role assignments for a specific\n * billing account. Calls global-rbac-svc directly via the same gateway URL\n * used by the rest of the billing module.\n *\n * Scope: `scopeType: \"billing\"`, `scopeId: <billingAccountId>`\n * Roles cloned per billing account at creation time:\n * - Billing Admin (full access to this billing account)\n * - Billing Viewer (read-only access to this billing account)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n PagePurpose,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, ActorIdentity, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useBillingAccounts } from '../../dashboard/hooks/useDashboard';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\nimport { useActorProfiles } from '../../../shared/hooks';\n\n// Basic UUID-v1-through-v5 shape — accept any 8-4-4-4-12 hex pattern.\n// We intentionally don't strictly validate the version nibble; the rbac\n// service is the authoritative ID validator.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Role {\n id: string;\n name: string;\n description?: string | null;\n priority: number;\n isSystem: boolean;\n}\n\ninterface ActorRoleAssignment {\n id: string;\n actorId: string;\n actorType: string;\n roleId: string;\n scopeId: string;\n isActive: boolean;\n expiresAt?: string | null;\n role?: Role;\n}\n\ninterface ActorRbacSummary {\n actorId: string;\n actorType: string;\n roleAssignments: ActorRoleAssignment[];\n effectivePermissionCount?: number;\n}\n\nconst ROLES_QUERY = `\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n`;\n\nconst ACTORS_QUERY = `\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n`;\n\nconst ASSIGN_ROLE_MUTATION = `\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n`;\n\nconst REMOVE_ROLE_MUTATION = `\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n`;\n\nexport const TeamPermissionsPage: FC = () => {\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Resolution priority: URL `?billingAccountId=` → persisted localStorage →\n // default/first available account. Pass `billingAccounts` so first-time\n // users with empty localStorage still land on the default account\n // (without it the hook's auto-select effect can't fire and the page\n // dead-ends on \"No billing account selected\").\n const { billingAccounts } = useBillingAccounts();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n\n // Share the resolved account with `useBillingPermissions` so its\n // billing-scope permissions fetch observes the same ID. Without this,\n // a first-time user (no URL param, empty localStorage) would see the\n // page resolve to the default account here while the permissions hook\n // still sat at null — billing-scope-only admins would see AccessDenied\n // until a navigation/reload propagated the localStorage write.\n const permissions = useBillingPermissions({ billingAccountIdOverride: billingAccountId });\n\n const [roles, setRoles] = useState<Role[]>([]);\n const [actors, setActors] = useState<ActorRbacSummary[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Add-member form\n const [newActorId, setNewActorId] = useState('');\n const [newRoleId, setNewRoleId] = useState('');\n const [isAdding, setIsAdding] = useState(false);\n const [removeAssignmentId, setRemoveAssignmentId] = useState<string | null>(null);\n\n const gqlPost = useCallback(\n async <T,>(query: string, variables: Record<string, unknown>): Promise<T> => {\n return directBillingGraphqlRequest<T>({\n apiGatewayUrl,\n authToken,\n orgId,\n query,\n variables,\n });\n },\n [apiGatewayUrl, authToken, orgId]\n );\n\n const loadData = useCallback(async () => {\n if (!billingAccountId) return;\n setIsLoading(true);\n setError(null);\n try {\n const [rolesResp, actorsResp] = await Promise.all([\n gqlPost<{ roles: Role[] }>(ROLES_QUERY, { scopeId: billingAccountId }),\n gqlPost<{ actors: ActorRbacSummary[] }>(ACTORS_QUERY, {\n scopeId: billingAccountId,\n }),\n ]);\n setRoles(rolesResp.roles ?? []);\n setActors(actorsResp.actors ?? []);\n // Default the role-picker to \"Billing Viewer\" if available, else first role\n const viewer = (rolesResp.roles ?? []).find((r) => /viewer/i.test(r.name));\n setNewRoleId(viewer?.id ?? rolesResp.roles?.[0]?.id ?? '');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load team data');\n } finally {\n setIsLoading(false);\n }\n }, [billingAccountId, gqlPost]);\n\n useEffect(() => {\n void loadData();\n }, [loadData]);\n\n const sortedActors = useMemo(\n () =>\n [...actors].sort((a, b) => {\n const priA = Math.max(...a.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n const priB = Math.max(...b.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n return priB - priA;\n }),\n [actors]\n );\n\n const profileInputs = useMemo(\n () =>\n sortedActors.map((a) => ({\n actorId: a.actorId,\n actorType: a.actorType,\n })),\n [sortedActors]\n );\n const profileMap = useActorProfiles(profileInputs);\n\n const billingAccountName = useMemo(() => {\n if (!billingAccountId) return undefined;\n return billingAccounts.find((acct) => acct.id === billingAccountId)?.name ?? undefined;\n }, [billingAccountId, billingAccounts]);\n\n const handleAddMember = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!billingAccountId || !newActorId.trim() || !newRoleId) return;\n const trimmedActorId = newActorId.trim();\n if (!UUID_RE.test(trimmedActorId)) {\n setError('User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.');\n return;\n }\n setIsAdding(true);\n setError(null);\n try {\n await gqlPost(ASSIGN_ROLE_MUTATION, {\n input: {\n actorId: trimmedActorId,\n actorType: 'USER',\n roleId: newRoleId,\n scopeId: billingAccountId,\n },\n });\n setNewActorId('');\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to assign role');\n } finally {\n setIsAdding(false);\n }\n };\n\n const handleRemoveAssignment = async (assignmentId: string) => {\n setError(null);\n try {\n await gqlPost(REMOVE_ROLE_MUTATION, { assignmentId });\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove role');\n }\n };\n\n if (!permissions.canManageBillingAccount) {\n return (\n <AccessDenied message=\"You don't have permission to manage team and permissions for this billing account.\" />\n );\n }\n\n if (!billingAccountId) {\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Select a billing account to manage who can access it.\"\n />\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No billing account selected\"\n description=\"Pick one from the billing account selector to manage its members and roles.\"\n />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Control who can manage this billing account and what they can do.\"\n />\n\n <PagePurpose>\n Decide who on your team can see and manage this billing account. Assign the Billing Admin\n role (full access) or Billing Viewer (read-only) to specific people, and revoke access when\n someone no longer needs it.\n </PagePurpose>\n\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 text-sm\">\n <span className=\"font-medium text-text-primary\">Billing account:</span>{' '}\n <span className=\"text-text-primary\">{billingAccountName ?? 'Unnamed account'}</span>\n {billingAccountId ? (\n <details className=\"mt-2 text-xs text-text-secondary\">\n <summary className=\"cursor-pointer\">Account ID</summary>\n <span className=\"mt-1 inline-block font-mono\">{billingAccountId}</span>\n </details>\n ) : null}\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-status-error-border/30 bg-status-error-bg/5 p-4 text-sm text-status-error-text\">\n {error}\n </div>\n )}\n\n {/* Add member form */}\n <form\n onSubmit={(e) => void handleAddMember(e)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-text-primary\">Add member</h3>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-3\">\n <div className=\"md:col-span-2\">\n <label htmlFor=\"newActorId\" className=\"block text-xs font-medium text-text-secondary\">\n User ID\n </label>\n <input\n id=\"newActorId\"\n type=\"text\"\n value={newActorId}\n onChange={(e) => setNewActorId(e.target.value)}\n placeholder=\"Paste the user ID from their profile page\"\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n required\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n Find this on the user's profile page.\n </p>\n </div>\n <div>\n <label htmlFor=\"newRoleId\" className=\"block text-xs font-medium text-text-secondary\">\n Role\n </label>\n <select\n id=\"newRoleId\"\n value={newRoleId}\n onChange={(e) => setNewRoleId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n required\n >\n {roles.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={isAdding || !newActorId.trim() || !newRoleId}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text disabled:opacity-50\"\n >\n {isAdding ? 'Adding…' : 'Add member'}\n </button>\n </form>\n\n {/* Members table */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"border-b border-border-subtle px-4 py-3\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Members</h3>\n <p className=\"text-xs text-text-secondary\">\n Each member's role applies only to this billing account.\n </p>\n </div>\n {isLoading ? (\n <div className=\"p-6 text-sm text-text-secondary\">Loading…</div>\n ) : sortedActors.length === 0 ? (\n <div className=\"p-6 text-sm text-text-secondary\">No members assigned yet.</div>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle text-left text-xs text-text-secondary\">\n <th className=\"px-4 py-2\">Member</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Roles</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {sortedActors.map((actor) => (\n <tr key={actor.actorId} className=\"border-b border-border-subtle last:border-0\">\n <td className=\"px-4 py-3\">\n <ActorIdentity\n profile={\n profileMap[actor.actorId] ?? {\n actorId: actor.actorId,\n actorType: actor.actorType,\n displayName:\n actor.actorType.toLowerCase() === 'user'\n ? `User ${actor.actorId.slice(0, 8)}…`\n : actor.actorType,\n secondaryLabel: actor.actorId.slice(0, 12),\n resolved: false,\n }\n }\n variant=\"cell\"\n />\n </td>\n <td className=\"px-4 py-3 text-xs capitalize text-text-secondary\">\n {actor.actorType.toLowerCase()}\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {actor.roleAssignments\n .filter((ra) => ra.isActive)\n .map((ra) => (\n <span\n key={ra.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border-subtle bg-bg-sunken px-2 py-0.5 text-xs\"\n >\n {ra.role?.name ?? ra.roleId}\n <button\n type=\"button\"\n onClick={() => setRemoveAssignmentId(ra.id)}\n className=\"text-text-secondary hover:text-status-error-text\"\n aria-label={`Remove ${ra.role?.name ?? 'role'}`}\n >\n ×\n </button>\n </span>\n ))}\n </div>\n </td>\n <td className=\"px-4 py-3 text-right\">\n <span className=\"text-xs text-text-secondary\">\n {actor.effectivePermissionCount ?? '—'} permissions\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {/* Roles reference */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Available roles</h3>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Roles are cloned from the system templates when a billing account is created.\n </p>\n <ul className=\"mt-3 space-y-2\">\n {roles.map((r) => (\n <li\n key={r.id}\n className=\"flex items-start gap-3 rounded-md border border-border-subtle bg-bg-surface p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-text-primary\">{r.name}</div>\n {r.description && (\n <p className=\"mt-1 text-xs text-text-secondary\">{r.description}</p>\n )}\n </div>\n <span className=\"text-xs text-text-secondary\">priority {r.priority}</span>\n </li>\n ))}\n </ul>\n </div>\n\n <AlertDialog\n open={removeAssignmentId !== null}\n onOpenChange={(open) => {\n if (!open) setRemoveAssignmentId(null);\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Remove role assignment?</AlertDialogTitle>\n <AlertDialogDescription>\n Remove this role assignment? The member will lose the access this role grants for this\n billing account.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={() => {\n const id = removeAssignmentId;\n setRemoveAssignmentId(null);\n if (id) void handleRemoveAssignment(id);\n }}\n >\n Remove\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Navigate as e, useParams as t } from "react-router";
|
|
2
2
|
import { jsx as n } from "react/jsx-runtime";
|
|
3
|
-
import { useI18n as r } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
4
3
|
//#region src/billing/modules/store/pages/StoreCheckoutPage.tsx
|
|
5
|
-
var
|
|
6
|
-
let {
|
|
7
|
-
return
|
|
8
|
-
to: `../checkout/store/${
|
|
4
|
+
var r = () => {
|
|
5
|
+
let { orderId: r } = t();
|
|
6
|
+
return r ? /* @__PURE__ */ n(e, {
|
|
7
|
+
to: `../checkout/store/${r}`,
|
|
9
8
|
replace: !0
|
|
10
9
|
}) : /* @__PURE__ */ n(e, {
|
|
11
10
|
to: "../checkout",
|
|
@@ -13,6 +12,6 @@ var i = () => {
|
|
|
13
12
|
});
|
|
14
13
|
};
|
|
15
14
|
//#endregion
|
|
16
|
-
export {
|
|
15
|
+
export { r as StoreCheckoutPage };
|
|
17
16
|
|
|
18
17
|
//# sourceMappingURL=StoreCheckoutPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoreCheckoutPage.js","names":[],"sources":["../../../../../src/billing/modules/store/pages/StoreCheckoutPage.tsx"],"sourcesContent":["import { type FC } from 'react';\nimport { Navigate, useParams } from 'react-router';\
|
|
1
|
+
{"version":3,"file":"StoreCheckoutPage.js","names":[],"sources":["../../../../../src/billing/modules/store/pages/StoreCheckoutPage.tsx"],"sourcesContent":["import { type FC } from 'react';\nimport { Navigate, useParams } from 'react-router';\n\n/**\n * Legacy route adapter.\n *\n * Historical links still point at `/billing/store/:orderId`, while the real checkout\n * flow now lives under `/billing/checkout/store/:storeOrderId` and is implemented by\n * the shared Checkout page.\n */\nexport const StoreCheckoutPage: FC = () => {\n const { orderId } = useParams<{ orderId: string }>();\n\n if (!orderId) {\n return <Navigate to=\"../checkout\" replace />;\n }\n\n return <Navigate to={`../checkout/store/${orderId}`} replace />;\n};\n"],"mappings":";;;AAUA,IAAa,UAA8B;CACzC,IAAM,EAAE,eAAY,EAA+B;CAMnD,OAJK,IAIE,kBAAC,GAAD;EAAU,IAAI,qBAAqB;EAAW,SAAA;CAAS,CAAA,IAHrD,kBAAC,GAAD;EAAU,IAAG;EAAc,SAAA;CAAS,CAAA;AAI/C"}
|