@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":"PromotionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/PromotionsListPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { Plus, Tag, Calendar, Users, ToggleLeft, ToggleRight, Gift } from 'lucide-react';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport {\n useListPromotionsQuery,\n useDeactivatePromotionMutation,\n useUpdatePromotionMutation,\n} from '../../../../generated/global-operations';\n\nexport const PromotionsListPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [showInactive, setShowInactive] = useState(false);\n const [actionId, setActionId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useListPromotionsQuery({\n variables: { includeInactive: showInactive },\n });\n\n const [deactivatePromotion] = useDeactivatePromotionMutation();\n const [updatePromotion] = useUpdatePromotionMutation();\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to manage promotions.\" />;\n }\n\n const promotions = data?.listPromotions ?? [];\n\n const handleToggle = async (id: string, currentlyActive: boolean) => {\n setActionId(id);\n try {\n if (currentlyActive) {\n await deactivatePromotion({ variables: { id } });\n } else {\n await updatePromotion({ variables: { id, input: { isActive: true } } });\n }\n await refetch();\n } finally {\n setActionId(null);\n }\n };\n\n const now = new Date();\n const statusLabel = (p: { isActive: boolean; startsAt: string; expiresAt: string }) => {\n if (!p.isActive) return { text: 'Inactive', cls: 'bg-bg-sunken text-text-muted' };\n if (new Date(p.expiresAt) < now)\n return { text: 'Expired', cls: 'bg-status-error-bg-subtle text-status-error-text' };\n if (new Date(p.startsAt) > now)\n return { text: 'Upcoming', cls: 'bg-[var(--color-accent-soft)] text-text-link' };\n return { text: 'Live', cls: 'bg-status-success-bg-subtle text-status-success-text' };\n };\n\n return (\n <div className=\"max-w-5xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center justify-between gap-4\">\n <PageHeader\n title=\"Promotions\"\n description=\"Time-bounded offers that grant bonus free renewal months to subscribers\"\n icon={<Gift className=\"size-6\" />}\n />\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"flex items-center gap-2 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover text-sm font-medium shrink-0 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Plus className=\"size-4\" />\n New Promotion\n </button>\n </div>\n\n <PagePurpose>\n Promotions are limited-time offers that reward subscribers with bonus free renewal months —\n great for launches or win-back campaigns. Set the bonus, the active window and an optional\n redemption cap, then activate or deactivate them as your campaign runs.\n </PagePurpose>\n\n {/* Filter */}\n <div className=\"flex items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-text-muted cursor-pointer select-none\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"rounded border-border-subtle\"\n />\n Show inactive / expired\n </label>\n </div>\n\n {loading ? (\n <div className=\"text-center py-16 text-text-muted\">Loading promotions…</div>\n ) : promotions.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No promotions yet\"\n description=\"Create one to offer bonus months to early subscribers\"\n action={\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text text-sm font-medium transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n Create Promotion\n </button>\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {promotions.map((p: (typeof promotions)[number]) => {\n const { text, cls } = statusLabel(p);\n const busy = actionId === p.id;\n return (\n <div\n key={p.id}\n className=\"bg-bg-surface border border-border-seam rounded-card p-5 flex flex-col sm:flex-row sm:items-center gap-4 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)]\"\n >\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span className=\"font-semibold text-text-primary truncate\">{p.name}</span>\n <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>\n {text}\n </span>\n </div>\n {p.description && (\n <p className=\"text-sm text-text-muted mb-2 line-clamp-1\">{p.description}</p>\n )}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-muted\">\n <span className=\"flex items-center gap-1\">\n <Gift className=\"size-3\" />\n <strong className=\"text-text-primary\">{p.bonusMonths}</strong> bonus month\n {p.bonusMonths !== 1 ? 's' : ''}\n </span>\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"size-3\" />\n {new Date(p.startsAt).toLocaleDateString()} –{' '}\n {new Date(p.expiresAt).toLocaleDateString()}\n </span>\n <span className=\"flex items-center gap-1\">\n <Users className=\"size-3\" />\n {p.currentRedemptions}\n {p.maxRedemptions ? ` / ${p.maxRedemptions}` : ''} redemptions\n </span>\n {p.applicableProductIds.length > 0 && (\n <span className=\"flex items-center gap-1\">\n <Tag className=\"size-3\" />\n {p.applicableProductIds.join(', ')}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 shrink-0\">\n <button\n onClick={() => handleToggle(p.id, p.isActive)}\n disabled={busy}\n title={p.isActive ? 'Deactivate' : 'Reactivate'}\n className={`p-2 rounded-button transition-colors ${\n p.isActive\n ? 'text-status-success-text hover:bg-status-success-bg-subtle'\n : 'text-text-muted hover:bg-bg-sunken'\n } disabled:opacity-50`}\n >\n {p.isActive ? (\n <ToggleRight className=\"size-5\" />\n ) : (\n <ToggleLeft className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PromotionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/PromotionsListPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { Plus, Tag, Calendar, Users, ToggleLeft, ToggleRight, Gift } from 'lucide-react';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport {\n useListPromotionsQuery,\n useDeactivatePromotionMutation,\n useUpdatePromotionMutation,\n} from '../../../../generated/global-operations';\n\nexport const PromotionsListPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [showInactive, setShowInactive] = useState(false);\n const [actionId, setActionId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useListPromotionsQuery({\n variables: { includeInactive: showInactive },\n });\n\n const [deactivatePromotion] = useDeactivatePromotionMutation();\n const [updatePromotion] = useUpdatePromotionMutation();\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to manage promotions.\" />;\n }\n\n const promotions = data?.listPromotions ?? [];\n\n const handleToggle = async (id: string, currentlyActive: boolean) => {\n setActionId(id);\n try {\n if (currentlyActive) {\n await deactivatePromotion({ variables: { id } });\n } else {\n await updatePromotion({ variables: { id, input: { isActive: true } } });\n }\n await refetch();\n } finally {\n setActionId(null);\n }\n };\n\n const now = new Date();\n const statusLabel = (p: { isActive: boolean; startsAt: string; expiresAt: string }) => {\n if (!p.isActive) return { text: 'Inactive', cls: 'bg-bg-sunken text-text-muted' };\n if (new Date(p.expiresAt) < now)\n return { text: 'Expired', cls: 'bg-status-error-bg-subtle text-status-error-text' };\n if (new Date(p.startsAt) > now)\n return { text: 'Upcoming', cls: 'bg-[var(--color-accent-soft)] text-text-link' };\n return { text: 'Live', cls: 'bg-status-success-bg-subtle text-status-success-text' };\n };\n\n return (\n <div className=\"max-w-5xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center justify-between gap-4\">\n <PageHeader\n title=\"Promotions\"\n description=\"Time-bounded offers that grant bonus free renewal months to subscribers\"\n icon={<Gift className=\"size-6\" />}\n />\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"flex items-center gap-2 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover text-sm font-medium shrink-0 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Plus className=\"size-4\" />\n New Promotion\n </button>\n </div>\n\n <PagePurpose>\n Promotions are limited-time offers that reward subscribers with bonus free renewal months —\n great for launches or win-back campaigns. Set the bonus, the active window and an optional\n redemption cap, then activate or deactivate them as your campaign runs.\n </PagePurpose>\n\n {/* Filter */}\n <div className=\"flex items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-text-muted cursor-pointer select-none\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"rounded border-border-subtle\"\n />\n Show inactive / expired\n </label>\n </div>\n\n {loading ? (\n <div className=\"text-center py-16 text-text-muted\">Loading promotions…</div>\n ) : promotions.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No promotions yet\"\n description=\"Create one to offer bonus months to early subscribers\"\n action={\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text text-sm font-medium transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n Create Promotion\n </button>\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {promotions.map((p: (typeof promotions)[number]) => {\n const { text, cls } = statusLabel(p);\n const busy = actionId === p.id;\n return (\n <div\n key={p.id}\n className=\"bg-bg-surface border border-border-seam rounded-card p-5 flex flex-col sm:flex-row sm:items-center gap-4 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)]\"\n >\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span className=\"font-semibold text-text-primary truncate\">{p.name}</span>\n <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>\n {text}\n </span>\n </div>\n {p.description && (\n <p className=\"text-sm text-text-muted mb-2 line-clamp-1\">{p.description}</p>\n )}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-muted\">\n <span className=\"flex items-center gap-1\">\n <Gift className=\"size-3\" />\n <strong className=\"text-text-primary\">{p.bonusMonths}</strong> bonus month\n {p.bonusMonths !== 1 ? 's' : ''}\n </span>\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"size-3\" />\n {new Date(p.startsAt).toLocaleDateString()} –{' '}\n {new Date(p.expiresAt).toLocaleDateString()}\n </span>\n <span className=\"flex items-center gap-1\">\n <Users className=\"size-3\" />\n {p.currentRedemptions}\n {p.maxRedemptions ? ` / ${p.maxRedemptions}` : ''} redemptions\n </span>\n {p.applicableProductIds.length > 0 && (\n <span className=\"flex items-center gap-1\">\n <Tag className=\"size-3\" />\n {p.applicableProductIds.join(', ')}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 shrink-0\">\n <button\n onClick={() => handleToggle(p.id, p.isActive)}\n disabled={busy}\n title={p.isActive ? 'Deactivate' : 'Reactivate'}\n className={`p-2 rounded-button transition-colors ${\n p.isActive\n ? 'text-status-success-text hover:bg-status-success-bg-subtle'\n : 'text-text-muted hover:bg-bg-sunken'\n } disabled:opacity-50`}\n >\n {p.isActive ? (\n <ToggleRight className=\"size-5\" />\n ) : (\n <ToggleLeft className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;AAYA,IAAa,UAA+B;CAC1C,IAAM,IAAa,EAAmB,GAChC,IAAc,EAAsB,GACpC,CAAC,GAAc,KAAmB,EAAS,EAAK,GAChD,CAAC,GAAU,KAAe,EAAwB,IAAI,GAEtD,EAAE,SAAM,YAAS,eAAY,EAAuB,EACxD,WAAW,EAAE,iBAAiB,EAAa,EAC7C,CAAC,GAEK,CAAC,KAAuB,EAA+B,GACvD,CAAC,KAAmB,EAA2B;CAErD,IAAI,CAAC,EAAY,gBACf,OAAO,kBAAC,GAAD,EAAc,SAAQ,kDAAmD,CAAA;CAGlF,IAAM,IAAa,GAAM,kBAAkB,CAAC,GAEtC,IAAe,OAAO,GAAY,MAA6B;EACnE,EAAY,CAAE;EACd,IAAI;GAMF,AALI,IACF,MAAM,EAAoB,EAAE,WAAW,EAAE,MAAG,EAAE,CAAC,IAE/C,MAAM,EAAgB,EAAE,WAAW;IAAE;IAAI,OAAO,EAAE,UAAU,GAAK;GAAE,EAAE,CAAC,GAExE,MAAM,EAAQ;EAChB,UAAU;GACR,EAAY,IAAI;EAClB;CACF,GAEM,oBAAM,IAAI,KAAK,GACf,KAAe,MACd,EAAE,WACH,IAAI,KAAK,EAAE,SAAS,IAAI,IACnB;EAAE,MAAM;EAAW,KAAK;CAAmD,IAChF,IAAI,KAAK,EAAE,QAAQ,IAAI,IAClB;EAAE,MAAM;EAAY,KAAK;CAA+C,IAC1E;EAAE,MAAM;EAAQ,KAAK;CAAuD,IAL3D;EAAE,MAAM;EAAY,KAAK;CAA+B;CAQlF,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,OAAM;KACN,aAAY;KACZ,MAAM,kBAAC,GAAD,EAAM,WAAU,SAAU,CAAA;IACjC,CAAA,GACD,kBAAC,UAAD;KACE,eAAe,EAAW,oBAAoB;KAC9C,WAAU;eAFZ,CAIE,kBAAC,GAAD,EAAM,WAAU,SAAU,CAAA,GAAC,eAErB;MACL;;GAEL,kBAAC,GAAD,EAAA,UAAa,iQAIA,CAAA;GAGb,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,SAAD;MACE,MAAK;MACL,SAAS;MACT,WAAW,MAAM,EAAgB,EAAE,OAAO,OAAO;MACjD,WAAU;KACX,CAAA,GAAC,yBAEG;;GACJ,CAAA;GAEJ,IACC,kBAAC,OAAD;IAAK,WAAU;cAAoC;GAAwB,CAAA,IACzE,EAAW,WAAW,IACxB,kBAAC,GAAD;IACE,cAAa;IACb,OAAM;IACN,aAAY;IACZ,QACE,kBAAC,UAAD;KACE,eAAe,EAAW,oBAAoB;KAC9C,WAAU;eACX;IAEO,CAAA;GAEX,CAAA,IAED,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAW,KAAK,MAAmC;KAClD,IAAM,EAAE,SAAM,WAAQ,EAAY,CAAC,GAC7B,IAAO,MAAa,EAAE;KAC5B,OACE,kBAAC,OAAD;MAEE,WAAU;gBAFZ,CAIE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAA4C,EAAE;SAAW,CAAA,GACzE,kBAAC,QAAD;UAAM,WAAW,gDAAgD;oBAC9D;SACG,CAAA,CACH;;QACJ,EAAE,eACD,kBAAC,KAAD;SAAG,WAAU;mBAA6C,EAAE;QAAe,CAAA;QAE7E,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,QAAD;WAAM,WAAU;qBAAhB;YACE,kBAAC,GAAD,EAAM,WAAU,SAAU,CAAA;YAC1B,kBAAC,UAAD;aAAQ,WAAU;uBAAqB,EAAE;YAAoB,CAAA;YAAC;YAC7D,EAAE,gBAAgB,IAAU,KAAN;WACnB;;UACN,kBAAC,QAAD;WAAM,WAAU;qBAAhB;YACE,kBAAC,GAAD,EAAU,WAAU,SAAU,CAAA;YAC7B,IAAI,KAAK,EAAE,QAAQ,EAAE,mBAAmB;YAAE;YAAG;YAC7C,IAAI,KAAK,EAAE,SAAS,EAAE,mBAAmB;WACtC;;UACN,kBAAC,QAAD;WAAM,WAAU;qBAAhB;YACE,kBAAC,GAAD,EAAO,WAAU,SAAU,CAAA;YAC1B,EAAE;YACF,EAAE,iBAAiB,MAAM,EAAE,mBAAmB;YAAG;WAC9C;;UACL,EAAE,qBAAqB,SAAS,KAC/B,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CACE,kBAAC,GAAD,EAAK,WAAU,SAAU,CAAA,GACxB,EAAE,qBAAqB,KAAK,IAAI,CAC7B;;SAEL;;OACF;UAEL,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,UAAD;QACE,eAAe,EAAa,EAAE,IAAI,EAAE,QAAQ;QAC5C,UAAU;QACV,OAAO,EAAE,WAAW,eAAe;QACnC,WAAW,wCACT,EAAE,WACE,+DACA,qCACL;kBAEA,EAAE,WACD,kBAAC,GAAD,EAAa,WAAU,SAAU,CAAA,IAEjC,kBAAC,GAAD,EAAY,WAAU,SAAU,CAAA;OAE5B,CAAA;MACL,CAAA,CACF;QAxDE,EAAE,EAwDJ;IAET,CAAC;GACE,CAAA;EAEJ;;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useQuotaTemplates.js","names":[],"sources":["../../../../../src/billing/modules/quota/hooks/useQuotaTemplates.ts"],"sourcesContent":["/**\n * Quota Module - useQuotaTemplates Hook\n * Provides access to quota template management (admin side)\n */\n\nimport { useApolloClient } from '@apollo/client/react';\nimport { useCallback } from 'react';\nimport {\n useGetAllQuotaTemplatesQuery,\n useCreateQuotaTemplateMutation,\n useUpdateQuotaTemplateMutation,\n GetAllQuotaTemplatesDocument,\n} from '../../../../generated/global-operations';\nimport type { Quota, CreateQuotaInput, UpdateQuotaInput } from '../../../shared/types';\n\ninterface UseQuotaTemplatesOptions {\n onlyActive?: boolean;\n}\n\ninterface UseQuotaTemplatesResult {\n quotas: Quota[];\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseQuotaTemplateResult {\n quota: Quota | null;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseQuotaTemplateMutationsResult {\n createQuota: (input: CreateQuotaInput) => Promise<boolean>;\n updateQuota: (input: UpdateQuotaInput) => Promise<boolean>;\n toggleQuota: (id: string, isActive: boolean) => Promise<boolean>;\n isCreating: boolean;\n isUpdating: boolean;\n isToggling: boolean;\n}\n\n/**\n * Hook for fetching all quota templates\n */\nexport function useQuotaTemplates(options: UseQuotaTemplatesOptions = {}): UseQuotaTemplatesResult {\n const { onlyActive = false } = options;\n\n const { data, loading, error, refetch } = useGetAllQuotaTemplatesQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n let quotas: Quota[] = data?.getAllQuotas || [];\n\n // Filter by active status if requested\n if (onlyActive) {\n quotas = quotas.filter((quota: Quota) => quota.isActive);\n }\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n quotas,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching a single quota template by ID\n */\nexport function useQuotaTemplate(quotaId: string | undefined): UseQuotaTemplateResult {\n const { data, loading, error, refetch } = useGetAllQuotaTemplatesQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const quotas: Quota[] = data?.getAllQuotas || [];\n const quota = quotas.find((q) => q.id === quotaId) || null;\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n quota,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for quota template mutations (create, update, toggle)\n */\nexport function useQuotaTemplateMutations(): UseQuotaTemplateMutationsResult {\n const client = useApolloClient();\n\n const [createQuotaMutation, { loading: isCreating }] = useCreateQuotaTemplateMutation();\n const [updateQuotaMutation, { loading: isUpdating }] = useUpdateQuotaTemplateMutation();\n const [toggleQuotaMutation, { loading: isToggling }] = useUpdateQuotaTemplateMutation();\n\n const createQuota = useCallback(\n async (input: CreateQuotaInput): Promise<boolean> => {\n try {\n const { data } = await createQuotaMutation({\n variables: { input },\n });\n // Refetch quotas list after creation\n await client.refetchQueries({ include: [GetAllQuotaTemplatesDocument] });\n return data?.createQuota ?? false;\n } catch (error) {\n console.error('Failed to create quota:', error);\n throw error;\n }\n },\n [createQuotaMutation, client]\n );\n\n const updateQuota = useCallback(\n async (input: UpdateQuotaInput): Promise<boolean> => {\n try {\n const { data } = await updateQuotaMutation({\n variables: { input },\n });\n // Refetch quotas list after update\n await client.refetchQueries({ include: [GetAllQuotaTemplatesDocument] });\n return data?.updateQuota ?? false;\n } catch (error) {\n console.error('Failed to update quota:', error);\n throw error;\n }\n },\n [updateQuotaMutation, client]\n );\n\n const toggleQuota = useCallback(\n async (id: string, isActive: boolean): Promise<boolean> => {\n try {\n const { data } = await toggleQuotaMutation({\n variables: {\n input: { id, isActive },\n },\n });\n // Refetch quotas list after toggle\n await client.refetchQueries({ include: [GetAllQuotaTemplatesDocument] });\n return data?.updateQuota ?? false;\n } catch (error) {\n console.error('Failed to toggle quota:', error);\n throw error;\n }\n },\n [toggleQuotaMutation, client]\n );\n\n return {\n createQuota,\n updateQuota,\n toggleQuota,\n isCreating,\n isUpdating,\n isToggling,\n };\n}\n"],"mappings":";;;;AA6CA,SAAgB,EAAkB,IAAoC,
|
|
1
|
+
{"version":3,"file":"useQuotaTemplates.js","names":[],"sources":["../../../../../src/billing/modules/quota/hooks/useQuotaTemplates.ts"],"sourcesContent":["/**\n * Quota Module - useQuotaTemplates Hook\n * Provides access to quota template management (admin side)\n */\n\nimport { useApolloClient } from '@apollo/client/react';\nimport { useCallback } from 'react';\nimport {\n useGetAllQuotaTemplatesQuery,\n useCreateQuotaTemplateMutation,\n useUpdateQuotaTemplateMutation,\n GetAllQuotaTemplatesDocument,\n} from '../../../../generated/global-operations';\nimport type { Quota, CreateQuotaInput, UpdateQuotaInput } from '../../../shared/types';\n\ninterface UseQuotaTemplatesOptions {\n onlyActive?: boolean;\n}\n\ninterface UseQuotaTemplatesResult {\n quotas: Quota[];\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseQuotaTemplateResult {\n quota: Quota | null;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseQuotaTemplateMutationsResult {\n createQuota: (input: CreateQuotaInput) => Promise<boolean>;\n updateQuota: (input: UpdateQuotaInput) => Promise<boolean>;\n toggleQuota: (id: string, isActive: boolean) => Promise<boolean>;\n isCreating: boolean;\n isUpdating: boolean;\n isToggling: boolean;\n}\n\n/**\n * Hook for fetching all quota templates\n */\nexport function useQuotaTemplates(options: UseQuotaTemplatesOptions = {}): UseQuotaTemplatesResult {\n const { onlyActive = false } = options;\n\n const { data, loading, error, refetch } = useGetAllQuotaTemplatesQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n let quotas: Quota[] = data?.getAllQuotas || [];\n\n // Filter by active status if requested\n if (onlyActive) {\n quotas = quotas.filter((quota: Quota) => quota.isActive);\n }\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n quotas,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching a single quota template by ID\n */\nexport function useQuotaTemplate(quotaId: string | undefined): UseQuotaTemplateResult {\n const { data, loading, error, refetch } = useGetAllQuotaTemplatesQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const quotas: Quota[] = data?.getAllQuotas || [];\n const quota = quotas.find((q) => q.id === quotaId) || null;\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n quota,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for quota template mutations (create, update, toggle)\n */\nexport function useQuotaTemplateMutations(): UseQuotaTemplateMutationsResult {\n const client = useApolloClient();\n\n const [createQuotaMutation, { loading: isCreating }] = useCreateQuotaTemplateMutation();\n const [updateQuotaMutation, { loading: isUpdating }] = useUpdateQuotaTemplateMutation();\n const [toggleQuotaMutation, { loading: isToggling }] = useUpdateQuotaTemplateMutation();\n\n const createQuota = useCallback(\n async (input: CreateQuotaInput): Promise<boolean> => {\n try {\n const { data } = await createQuotaMutation({\n variables: { input },\n });\n // Refetch quotas list after creation\n await client.refetchQueries({ include: [GetAllQuotaTemplatesDocument] });\n return data?.createQuota ?? false;\n } catch (error) {\n console.error('Failed to create quota:', error);\n throw error;\n }\n },\n [createQuotaMutation, client]\n );\n\n const updateQuota = useCallback(\n async (input: UpdateQuotaInput): Promise<boolean> => {\n try {\n const { data } = await updateQuotaMutation({\n variables: { input },\n });\n // Refetch quotas list after update\n await client.refetchQueries({ include: [GetAllQuotaTemplatesDocument] });\n return data?.updateQuota ?? false;\n } catch (error) {\n console.error('Failed to update quota:', error);\n throw error;\n }\n },\n [updateQuotaMutation, client]\n );\n\n const toggleQuota = useCallback(\n async (id: string, isActive: boolean): Promise<boolean> => {\n try {\n const { data } = await toggleQuotaMutation({\n variables: {\n input: { id, isActive },\n },\n });\n // Refetch quotas list after toggle\n await client.refetchQueries({ include: [GetAllQuotaTemplatesDocument] });\n return data?.updateQuota ?? false;\n } catch (error) {\n console.error('Failed to toggle quota:', error);\n throw error;\n }\n },\n [toggleQuotaMutation, client]\n );\n\n return {\n createQuota,\n updateQuota,\n toggleQuota,\n isCreating,\n isUpdating,\n isToggling,\n };\n}\n"],"mappings":";;;;AA6CA,SAAgB,EAAkB,IAAoC,CAAC,GAA4B;CACjG,IAAM,EAAE,gBAAa,OAAU,GAEzB,EAAE,SAAM,YAAS,UAAO,eAAY,EAA6B,EACrE,aAAa,oBACf,CAAC,GAEG,IAAkB,GAAM,gBAAgB,CAAC;CAG7C,AAAI,MACF,IAAS,EAAO,QAAQ,MAAiB,EAAM,QAAQ;CAGzD,IAAM,IAAgB,EAAY,YAAY;EAC5C,MAAM,EAAQ;CAChB,GAAG,CAAC,CAAO,CAAC;CAEZ,OAAO;EACL;EACA,WAAW;EACJ;EACP,SAAS;CACX;AACF;AAKA,SAAgB,EAAiB,GAAqD;CACpF,IAAM,EAAE,SAAM,YAAS,UAAO,eAAY,EAA6B,EACrE,aAAa,oBACf,CAAC;CASD,OAAO;EACL,QARsB,GAAM,gBAAgB,CAAC,GAC1B,MAAM,MAAM,EAAE,OAAO,CAAO,KAAK;EAQpD,WAAW;EACJ;EACP,SARoB,EAAY,YAAY;GAC5C,MAAM,EAAQ;EAChB,GAAG,CAAC,CAAO,CAMA;CACX;AACF;AAKA,SAAgB,IAA6D;CAC3E,IAAM,IAAS,EAAgB,GAEzB,CAAC,GAAqB,EAAE,SAAS,OAAgB,EAA+B,GAChF,CAAC,GAAqB,EAAE,SAAS,OAAgB,EAA+B,GAChF,CAAC,GAAqB,EAAE,SAAS,OAAgB,EAA+B;CAuDtF,OAAO;EACL,aAtDkB,EAClB,OAAO,MAA8C;GACnD,IAAI;IACF,IAAM,EAAE,YAAS,MAAM,EAAoB,EACzC,WAAW,EAAE,SAAM,EACrB,CAAC;IAGD,OADA,MAAM,EAAO,eAAe,EAAE,SAAS,CAAC,CAA4B,EAAE,CAAC,GAChE,GAAM,eAAe;GAC9B,SAAS,GAAO;IAEd,MADA,QAAQ,MAAM,2BAA2B,CAAK,GACxC;GACR;EACF,GACA,CAAC,GAAqB,CAAM,CAwC5B;EACA,aAtCkB,EAClB,OAAO,MAA8C;GACnD,IAAI;IACF,IAAM,EAAE,YAAS,MAAM,EAAoB,EACzC,WAAW,EAAE,SAAM,EACrB,CAAC;IAGD,OADA,MAAM,EAAO,eAAe,EAAE,SAAS,CAAC,CAA4B,EAAE,CAAC,GAChE,GAAM,eAAe;GAC9B,SAAS,GAAO;IAEd,MADA,QAAQ,MAAM,2BAA2B,CAAK,GACxC;GACR;EACF,GACA,CAAC,GAAqB,CAAM,CAwB5B;EACA,aAtBkB,EAClB,OAAO,GAAY,MAAwC;GACzD,IAAI;IACF,IAAM,EAAE,YAAS,MAAM,EAAoB,EACzC,WAAW,EACT,OAAO;KAAE;KAAI;IAAS,EACxB,EACF,CAAC;IAGD,OADA,MAAM,EAAO,eAAe,EAAE,SAAS,CAAC,CAA4B,EAAE,CAAC,GAChE,GAAM,eAAe;GAC9B,SAAS,GAAO;IAEd,MADA,QAAQ,MAAM,2BAA2B,CAAK,GACxC;GACR;EACF,GACA,CAAC,GAAqB,CAAM,CAM5B;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuotaAssignFreePage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaAssignFreePage.tsx"],"sourcesContent":["/**\n * Quota Module - Assign Quota Free (Admin only)\n *\n * Internal tool for assigning a single quota template directly to a billing\n * account (and, for workspace-scoped quotas, a workspace) at no charge and\n * without purchasing a whole plan. Protected by the same team secret as\n * \"Assign Plan Free\" — never expose to end users.\n *\n * Route: /billing/quota/assign-free\n */\n\nimport { useMemo, useState, type FC, type FormEvent } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useAssignQuotaFreeMutation } from '../../../../generated/global-operations';\nimport { useQuotaTemplates } from '../hooks';\n\nexport const QuotaAssignFreePage: 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\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n\n const [secret, setSecret] = useState('');\n const [billingAccountId, setBillingAccountId] = useState('');\n const [quotaId, setQuotaId] = useState('');\n const [workspaceId, setWorkspaceId] = useState('');\n const [limit, setLimit] = useState('');\n const [durationDays, setDurationDays] = useState('');\n const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);\n\n const { quotas, isLoading: isQuotasLoading } = useQuotaTemplates({ onlyActive: true });\n const [assignQuotaFree, { loading: isAssigning }] = useAssignQuotaFreeMutation();\n\n // Scope comes from the quota template itself, not an independent toggle —\n // this mirrors the backend, which routes on quota.scope and ignores any\n // client-supplied scope, so the UI can't drift from what will actually happen.\n const selectedQuota = quotas.find((q) => q.id === quotaId);\n const isWorkspaceScoped = selectedQuota?.scope === 'WORKSPACE';\n\n const defaultLimit = useMemo(() => {\n const limits = selectedQuota?.limits as { value?: number } | undefined;\n return limits?.value;\n }, [selectedQuota]);\n\n if (!permissions.canManageQuotas) {\n return (\n <AccessDenied\n message={tr(\n 'billing.quotaAssignFree.noPermission',\n \"You don't have permission to access this page.\"\n )}\n />\n );\n }\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setResult(null);\n\n if (!secret.trim() || !billingAccountId.trim() || !quotaId) {\n setResult({ success: false, message: 'All fields are required.' });\n return;\n }\n\n try {\n const { data } = await assignQuotaFree({\n variables: {\n secret: secret.trim(),\n billingAccountId: billingAccountId.trim(),\n quotaId,\n ...(workspaceId.trim() ? { workspaceId: workspaceId.trim() } : {}),\n ...(limit.trim() ? { limit: Number(limit) } : {}),\n ...(durationDays.trim() ? { durationDays: Number(durationDays) } : {}),\n },\n });\n\n if (data?.assignQuotaFree) {\n const assigned = data.assignQuotaFree;\n setResult({\n success: true,\n message:\n `Quota \"${assigned.quotaName}\" assigned (${assigned.scope}). Limit: ${assigned.limit ?? 'default'}` +\n (assigned.workspaceId ? ` · Workspace: ${assigned.workspaceId}` : '') +\n (assigned.endtime\n ? ` · Valid until: ${new Date(assigned.endtime).toLocaleDateString()}`\n : ''),\n });\n // Clear sensitive fields\n setSecret('');\n setBillingAccountId('');\n setQuotaId('');\n setWorkspaceId('');\n setLimit('');\n setDurationDays('');\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to assign quota.';\n setResult({ success: false, message: msg });\n }\n };\n\n return (\n <div className=\"space-y-6 p-6 max-w-2xl\">\n <PageHeader\n title={tr('billing.quotaAssignFree.title', 'Assign Quota for Free')}\n description={tr(\n 'billing.quotaAssignFree.description',\n 'Internally assign a single quota to a billing account at no charge. Requires the team secret.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/quota')}\n className=\"px-3 py-2 text-sm border border-border-subtle rounded-md text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n ← Back to Quotas\n </button>\n }\n />\n\n <PagePurpose>\n {tr(\n 'billing.quotaAssignFree.purpose',\n 'An internal tool to grant a single quota to a billing account without assigning a whole plan — useful for one-off comps or fixing a missing entitlement. It bypasses payment, so it requires the team secret and should be used deliberately.'\n )}\n </PagePurpose>\n\n {/* Warning banner */}\n <div className=\"flex items-start gap-3 p-4 rounded-lg border border-status-warning-border bg-status-warning-bg/10\">\n <svg\n className=\"size-5 text-status-warning-text 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-2.5L13.732 4c-.77-.833-1.962-.833-2.732 0L3.072 16.5c-.77.833.192 2.5 1.732 2.5z\"\n />\n </svg>\n <div>\n <p className=\"text-sm font-medium text-status-warning-text\">Internal admin tool</p>\n <p className=\"text-xs text-status-warning-text/80 mt-0.5\">\n This bypasses all payment flows. The quota is granted immediately with no charge. Use\n only for enterprise deals, comped accounts, or fixing a missing entitlement.\n </p>\n </div>\n </div>\n\n <form\n onSubmit={handleSubmit}\n className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\"\n >\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Assignment Details</h2>\n </div>\n\n <div className=\"p-4 space-y-4\">\n {/* Secret */}\n <div>\n <label htmlFor=\"secret\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Team Secret <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"secret\"\n type=\"password\"\n value={secret}\n onChange={(e) => setSecret(e.target.value)}\n autoComplete=\"off\"\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] font-mono\"\n placeholder=\"Enter PLAN_ASSIGN_SECRET\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Same team secret used by Assign Plan Free. Never expose this to users.\n </p>\n </div>\n\n {/* Billing Account ID */}\n <div>\n <label\n htmlFor=\"billingAccountId\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Billing Account ID <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"billingAccountId\"\n type=\"text\"\n value={billingAccountId}\n onChange={(e) => setBillingAccountId(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] font-mono text-sm\"\n placeholder=\"e.g. ba_01jk3n2...\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Find this in the user's billing account settings or via the billing accounts list.\n </p>\n </div>\n\n {/* Quota selector */}\n <div>\n <label htmlFor=\"quotaId\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Quota <span className=\"text-status-error-text\">*</span>\n </label>\n {isQuotasLoading ? (\n <div className=\"h-10 bg-bg-sunken animate-pulse rounded-md\" />\n ) : (\n <select\n id=\"quotaId\"\n value={quotaId}\n onChange={(e) => {\n setQuotaId(e.target.value);\n setWorkspaceId('');\n }}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n required\n >\n <option value=\"\" disabled>\n Select a quota…\n </option>\n {quotas.map((quota) => (\n <option key={quota.id} value={quota.id}>\n {quota.name} ({quota.scope ?? 'WORKSPACE'})\n </option>\n ))}\n </select>\n )}\n {selectedQuota && (\n <p className=\"text-xs text-text-muted mt-1\">\n Scope: {selectedQuota.scope ?? 'WORKSPACE'} · Type: {selectedQuota.quotaType}\n {defaultLimit != null ? ` · Default limit: ${defaultLimit}` : ''}\n </p>\n )}\n </div>\n\n {/* Workspace ID — required for WORKSPACE-scoped quotas, hidden otherwise */}\n {isWorkspaceScoped && (\n <div>\n <label\n htmlFor=\"workspaceId\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Workspace ID <span className=\"text-text-secondary font-normal\">(optional)</span>\n </label>\n <input\n id=\"workspaceId\"\n type=\"text\"\n value={workspaceId}\n onChange={(e) => setWorkspaceId(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-md bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-primary font-mono text-sm\"\n placeholder=\"e.g. ws_01jk3n2… (leave blank to auto-resolve)\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">\n This quota is workspace-scoped. If blank, the first active workspace for this org is\n used automatically.\n </p>\n </div>\n )}\n\n {/* Limit override (optional) */}\n <div>\n <label htmlFor=\"limit\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Limit <span className=\"text-text-secondary font-normal\">(optional)</span>\n </label>\n <input\n id=\"limit\"\n type=\"number\"\n value={limit}\n onChange={(e) => setLimit(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder={\n defaultLimit != null\n ? `Leave blank to use default (${defaultLimit})`\n : 'Leave blank to use the quota default'\n }\n />\n </div>\n\n {/* Duration in days (optional, GLOBAL scope only) */}\n {!isWorkspaceScoped && selectedQuota && (\n <div>\n <label\n htmlFor=\"durationDays\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Duration (days) <span className=\"text-text-secondary font-normal\">(optional)</span>\n </label>\n <input\n id=\"durationDays\"\n type=\"number\"\n min={1}\n value={durationDays}\n onChange={(e) => setDurationDays(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"Leave blank for 365 days\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Ignored if this quota attaches to an existing subscription — it then expires with\n that subscription instead.\n </p>\n </div>\n )}\n </div>\n\n {/* Result banner */}\n {result && (\n <div\n className={`mx-4 mb-4 p-3 rounded-lg border text-sm ${\n result.success\n ? 'border-status-success-border bg-status-success-bg/10 text-status-success-text'\n : 'border-status-error-border bg-status-error-bg/10 text-status-error-text'\n }`}\n >\n {result.message}\n </div>\n )}\n\n <div className=\"px-4 pb-4 flex items-center gap-3\">\n <button\n type=\"submit\"\n disabled={isAssigning || !secret || !billingAccountId || !quotaId}\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 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isAssigning ? 'Assigning…' : 'Assign Quota for Free'}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setSecret('');\n setBillingAccountId('');\n setQuotaId('');\n setWorkspaceId('');\n setLimit('');\n setDurationDays('');\n setResult(null);\n }}\n className=\"px-4 py-2 text-sm border border-border-subtle rounded-md text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n Clear\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"QuotaAssignFreePage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaAssignFreePage.tsx"],"sourcesContent":["/**\n * Quota Module - Assign Quota Free (Admin only)\n *\n * Internal tool for assigning a single quota template directly to a billing\n * account (and, for workspace-scoped quotas, a workspace) at no charge and\n * without purchasing a whole plan. Protected by the same team secret as\n * \"Assign Plan Free\" — never expose to end users.\n *\n * Route: /billing/quota/assign-free\n */\n\nimport { useMemo, useState, type FC, type FormEvent } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useAssignQuotaFreeMutation } from '../../../../generated/global-operations';\nimport { useQuotaTemplates } from '../hooks';\n\nexport const QuotaAssignFreePage: 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\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n\n const [secret, setSecret] = useState('');\n const [billingAccountId, setBillingAccountId] = useState('');\n const [quotaId, setQuotaId] = useState('');\n const [workspaceId, setWorkspaceId] = useState('');\n const [limit, setLimit] = useState('');\n const [durationDays, setDurationDays] = useState('');\n const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);\n\n const { quotas, isLoading: isQuotasLoading } = useQuotaTemplates({ onlyActive: true });\n const [assignQuotaFree, { loading: isAssigning }] = useAssignQuotaFreeMutation();\n\n // Scope comes from the quota template itself, not an independent toggle —\n // this mirrors the backend, which routes on quota.scope and ignores any\n // client-supplied scope, so the UI can't drift from what will actually happen.\n const selectedQuota = quotas.find((q) => q.id === quotaId);\n const isWorkspaceScoped = selectedQuota?.scope === 'WORKSPACE';\n\n const defaultLimit = useMemo(() => {\n const limits = selectedQuota?.limits as { value?: number } | undefined;\n return limits?.value;\n }, [selectedQuota]);\n\n if (!permissions.canManageQuotas) {\n return (\n <AccessDenied\n message={tr(\n 'billing.quotaAssignFree.noPermission',\n \"You don't have permission to access this page.\"\n )}\n />\n );\n }\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setResult(null);\n\n if (!secret.trim() || !billingAccountId.trim() || !quotaId) {\n setResult({ success: false, message: 'All fields are required.' });\n return;\n }\n\n try {\n const { data } = await assignQuotaFree({\n variables: {\n secret: secret.trim(),\n billingAccountId: billingAccountId.trim(),\n quotaId,\n ...(workspaceId.trim() ? { workspaceId: workspaceId.trim() } : {}),\n ...(limit.trim() ? { limit: Number(limit) } : {}),\n ...(durationDays.trim() ? { durationDays: Number(durationDays) } : {}),\n },\n });\n\n if (data?.assignQuotaFree) {\n const assigned = data.assignQuotaFree;\n setResult({\n success: true,\n message:\n `Quota \"${assigned.quotaName}\" assigned (${assigned.scope}). Limit: ${assigned.limit ?? 'default'}` +\n (assigned.workspaceId ? ` · Workspace: ${assigned.workspaceId}` : '') +\n (assigned.endtime\n ? ` · Valid until: ${new Date(assigned.endtime).toLocaleDateString()}`\n : ''),\n });\n // Clear sensitive fields\n setSecret('');\n setBillingAccountId('');\n setQuotaId('');\n setWorkspaceId('');\n setLimit('');\n setDurationDays('');\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to assign quota.';\n setResult({ success: false, message: msg });\n }\n };\n\n return (\n <div className=\"space-y-6 p-6 max-w-2xl\">\n <PageHeader\n title={tr('billing.quotaAssignFree.title', 'Assign Quota for Free')}\n description={tr(\n 'billing.quotaAssignFree.description',\n 'Internally assign a single quota to a billing account at no charge. Requires the team secret.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/quota')}\n className=\"px-3 py-2 text-sm border border-border-subtle rounded-md text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n ← Back to Quotas\n </button>\n }\n />\n\n <PagePurpose>\n {tr(\n 'billing.quotaAssignFree.purpose',\n 'An internal tool to grant a single quota to a billing account without assigning a whole plan — useful for one-off comps or fixing a missing entitlement. It bypasses payment, so it requires the team secret and should be used deliberately.'\n )}\n </PagePurpose>\n\n {/* Warning banner */}\n <div className=\"flex items-start gap-3 p-4 rounded-lg border border-status-warning-border bg-status-warning-bg/10\">\n <svg\n className=\"size-5 text-status-warning-text 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-2.5L13.732 4c-.77-.833-1.962-.833-2.732 0L3.072 16.5c-.77.833.192 2.5 1.732 2.5z\"\n />\n </svg>\n <div>\n <p className=\"text-sm font-medium text-status-warning-text\">Internal admin tool</p>\n <p className=\"text-xs text-status-warning-text/80 mt-0.5\">\n This bypasses all payment flows. The quota is granted immediately with no charge. Use\n only for enterprise deals, comped accounts, or fixing a missing entitlement.\n </p>\n </div>\n </div>\n\n <form\n onSubmit={handleSubmit}\n className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\"\n >\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Assignment Details</h2>\n </div>\n\n <div className=\"p-4 space-y-4\">\n {/* Secret */}\n <div>\n <label htmlFor=\"secret\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Team Secret <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"secret\"\n type=\"password\"\n value={secret}\n onChange={(e) => setSecret(e.target.value)}\n autoComplete=\"off\"\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] font-mono\"\n placeholder=\"Enter PLAN_ASSIGN_SECRET\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Same team secret used by Assign Plan Free. Never expose this to users.\n </p>\n </div>\n\n {/* Billing Account ID */}\n <div>\n <label\n htmlFor=\"billingAccountId\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Billing Account ID <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"billingAccountId\"\n type=\"text\"\n value={billingAccountId}\n onChange={(e) => setBillingAccountId(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] font-mono text-sm\"\n placeholder=\"e.g. ba_01jk3n2...\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Find this in the user's billing account settings or via the billing accounts list.\n </p>\n </div>\n\n {/* Quota selector */}\n <div>\n <label htmlFor=\"quotaId\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Quota <span className=\"text-status-error-text\">*</span>\n </label>\n {isQuotasLoading ? (\n <div className=\"h-10 bg-bg-sunken animate-pulse rounded-md\" />\n ) : (\n <select\n id=\"quotaId\"\n value={quotaId}\n onChange={(e) => {\n setQuotaId(e.target.value);\n setWorkspaceId('');\n }}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n required\n >\n <option value=\"\" disabled>\n Select a quota…\n </option>\n {quotas.map((quota) => (\n <option key={quota.id} value={quota.id}>\n {quota.name} ({quota.scope ?? 'WORKSPACE'})\n </option>\n ))}\n </select>\n )}\n {selectedQuota && (\n <p className=\"text-xs text-text-muted mt-1\">\n Scope: {selectedQuota.scope ?? 'WORKSPACE'} · Type: {selectedQuota.quotaType}\n {defaultLimit != null ? ` · Default limit: ${defaultLimit}` : ''}\n </p>\n )}\n </div>\n\n {/* Workspace ID — required for WORKSPACE-scoped quotas, hidden otherwise */}\n {isWorkspaceScoped && (\n <div>\n <label\n htmlFor=\"workspaceId\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Workspace ID <span className=\"text-text-secondary font-normal\">(optional)</span>\n </label>\n <input\n id=\"workspaceId\"\n type=\"text\"\n value={workspaceId}\n onChange={(e) => setWorkspaceId(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-md bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-primary font-mono text-sm\"\n placeholder=\"e.g. ws_01jk3n2… (leave blank to auto-resolve)\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">\n This quota is workspace-scoped. If blank, the first active workspace for this org is\n used automatically.\n </p>\n </div>\n )}\n\n {/* Limit override (optional) */}\n <div>\n <label htmlFor=\"limit\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Limit <span className=\"text-text-secondary font-normal\">(optional)</span>\n </label>\n <input\n id=\"limit\"\n type=\"number\"\n value={limit}\n onChange={(e) => setLimit(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder={\n defaultLimit != null\n ? `Leave blank to use default (${defaultLimit})`\n : 'Leave blank to use the quota default'\n }\n />\n </div>\n\n {/* Duration in days (optional, GLOBAL scope only) */}\n {!isWorkspaceScoped && selectedQuota && (\n <div>\n <label\n htmlFor=\"durationDays\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Duration (days) <span className=\"text-text-secondary font-normal\">(optional)</span>\n </label>\n <input\n id=\"durationDays\"\n type=\"number\"\n min={1}\n value={durationDays}\n onChange={(e) => setDurationDays(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"Leave blank for 365 days\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Ignored if this quota attaches to an existing subscription — it then expires with\n that subscription instead.\n </p>\n </div>\n )}\n </div>\n\n {/* Result banner */}\n {result && (\n <div\n className={`mx-4 mb-4 p-3 rounded-lg border text-sm ${\n result.success\n ? 'border-status-success-border bg-status-success-bg/10 text-status-success-text'\n : 'border-status-error-border bg-status-error-bg/10 text-status-error-text'\n }`}\n >\n {result.message}\n </div>\n )}\n\n <div className=\"px-4 pb-4 flex items-center gap-3\">\n <button\n type=\"submit\"\n disabled={isAssigning || !secret || !billingAccountId || !quotaId}\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 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isAssigning ? 'Assigning…' : 'Assign Quota for Free'}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setSecret('');\n setBillingAccountId('');\n setQuotaId('');\n setWorkspaceId('');\n setLimit('');\n setDurationDays('');\n setResult(null);\n }}\n className=\"px-4 py-2 text-sm border border-border-subtle rounded-md text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n Clear\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuotaTemplateCreatePage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaTemplateCreatePage.tsx"],"sourcesContent":["/**\n * Quota Module - Quota Template Create Page\n * Form to create a new quota template\n */\n\nimport { useState, type FC, type FormEvent } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useQuotaTemplateMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { QuotaType, QuotaResetPeriod, type CreateQuotaInput } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport const QuotaTemplateCreatePage: 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 { createQuota, isCreating } = useQuotaTemplateMutations();\n\n // Form state\n const [name, setName] = useState('');\n const [displayName, setDisplayName] = useState('');\n const [description, setDescription] = useState('');\n const [limitType, setLimitType] = useState('numeric');\n const [limitValue, setLimitValue] = useState('');\n const [unit, setUnit] = useState('');\n const [quotaType, setQuotaType] = useState<QuotaType>(QuotaType.PRODUCT_SPECIFIC);\n const [reusable, setReusable] = useState(false);\n const [resetPeriod, setResetPeriod] = useState<QuotaResetPeriod>(QuotaResetPeriod.NEVER);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Permission check\n if (!permissions.canManageQuotas) {\n return <AccessDenied message=\"You don't have permission to create quotas.\" />;\n }\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n setSuccess(false);\n\n // Validation\n if (!name.trim()) {\n setError('Quota name is required');\n return;\n }\n if (!limitValue || isNaN(Number(limitValue)) || Number(limitValue) < 0) {\n setError('Valid limit value is required');\n return;\n }\n if (!unit.trim()) {\n setError('Unit is required (e.g., bots, GB, seats)');\n return;\n }\n\n const input: CreateQuotaInput = {\n name: name.trim(),\n limits: {\n type: limitType,\n value: Number(limitValue),\n unit: unit.trim(),\n },\n quotaType,\n reusable,\n resetPeriod,\n context: {\n displayName: displayName.trim() || name.trim(),\n description: description.trim(),\n },\n };\n\n try {\n await createQuota(input);\n setSuccess(true);\n // Navigate back after short delay\n setTimeout(() => navigateTo('/quotas'), 1500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create quota');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader title=\"Create New Quota\" description=\"Set up a new quota template\" />\n\n <PagePurpose>\n Define a reusable limit — its name, type (pooled or product-specific), unit and default\n value — that you can then attach to any number of plans and add-ons. Build the template once\n here instead of redefining the same limit on every plan.\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-6\">\n {/* Error/Success Messages */}\n {error && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n {success && (\n <div className=\"p-4 bg-status-success-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-success-text\">\n Quota created successfully! Redirecting…\n </p>\n </div>\n )}\n\n {/* Basic Info */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Basic Information</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Machine Key (name) */}\n <div>\n <label htmlFor=\"name\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Quota Key *\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] font-mono text-sm\"\n placeholder=\"e.g., sessions-started, tunnels-created\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Internal machine key, must match the <code>quotaName</code> used in{' '}\n <code>@usage</code> directives. Never shown to users.\n </p>\n </div>\n\n {/* Display Name */}\n <div>\n <label\n htmlFor=\"displayName\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Display Name *\n </label>\n <input\n id=\"displayName\"\n type=\"text\"\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"e.g., Tunnels, Session Starts, Webhooks\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Human-readable name shown to users in billing and usage UI.\n </p>\n </div>\n\n {/* Description */}\n <div>\n <label\n htmlFor=\"description\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Description\n </label>\n <textarea\n id=\"description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] resize-none\"\n placeholder=\"e.g., Active tunnels created through VibeControls (slot freed on delete)\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Short explanation shown in billing and usage detail views.\n </p>\n </div>\n\n {/* Quota Type */}\n <div>\n <label\n htmlFor=\"quotaType\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Quota Type *\n </label>\n <select\n id=\"quotaType\"\n value={quotaType}\n onChange={(e) => setQuotaType(e.target.value as QuotaType)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value={QuotaType.PRODUCT_SPECIFIC}>Product Specific</option>\n <option value={QuotaType.POOLED}>Pooled</option>\n </select>\n <p className=\"text-xs text-text-muted mt-1\">\n {quotaType === QuotaType.POOLED\n ? 'Shared across all products'\n : 'Specific to individual products'}\n </p>\n </div>\n\n {/* Reusable */}\n <div className=\"flex items-center gap-3\">\n <input\n id=\"reusable\"\n type=\"checkbox\"\n checked={reusable}\n onChange={(e) => setReusable(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <div>\n <label htmlFor=\"reusable\" className=\"text-sm font-medium text-text-primary\">\n Reusable Quota\n </label>\n <p className=\"text-xs text-text-muted\">\n Enable for quotas like seats or bots that can be freed up\n </p>\n </div>\n </div>\n\n {/* Reset Period */}\n <div>\n <label\n htmlFor=\"resetPeriod\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Reset Period\n </label>\n <select\n id=\"resetPeriod\"\n value={resetPeriod}\n onChange={(e) => setResetPeriod(e.target.value as QuotaResetPeriod)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value={QuotaResetPeriod.NEVER}>Never (one-time limit)</option>\n <option value={QuotaResetPeriod.DAILY}>Daily (resets every day)</option>\n <option value={QuotaResetPeriod.MONTHLY}>Monthly (resets every month)</option>\n </select>\n <p className=\"text-xs text-text-muted mt-1\">\n How often the quota usage counter is reset to zero\n </p>\n </div>\n </div>\n </div>\n\n {/* Limits Configuration */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Limits Configuration</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Limit Type */}\n <div>\n <label\n htmlFor=\"limitType\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Limit Type *\n </label>\n <select\n id=\"limitType\"\n value={limitType}\n onChange={(e) => setLimitType(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"numeric\">Numeric (count)</option>\n <option value=\"storage\">Storage (bytes)</option>\n <option value=\"duration\">Duration (seconds)</option>\n <option value=\"percentage\">Percentage</option>\n </select>\n </div>\n\n {/* Limit Value */}\n <div>\n <label\n htmlFor=\"limitValue\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Limit Value *\n </label>\n <input\n id=\"limitValue\"\n type=\"number\"\n min=\"0\"\n value={limitValue}\n onChange={(e) => setLimitValue(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"1000\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">The maximum limit value for this quota</p>\n </div>\n\n {/* Unit */}\n <div>\n <label htmlFor=\"unit\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Unit *\n </label>\n <input\n id=\"unit\"\n type=\"text\"\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"e.g., bots, GB, MB, seats, workflow runs\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n The unit of measurement displayed with usage values (e.g., "2/6 bots")\n </p>\n </div>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center gap-3 pt-4\">\n <button\n type=\"submit\"\n disabled={isCreating}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors disabled:opacity-50\"\n >\n {isCreating\n ? tr('billing.quota.creating', 'Creating...')\n : tr('billing.quota.createQuota', 'Create Quota')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas')}\n className=\"px-6 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Cancel\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"QuotaTemplateCreatePage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaTemplateCreatePage.tsx"],"sourcesContent":["/**\n * Quota Module - Quota Template Create Page\n * Form to create a new quota template\n */\n\nimport { useState, type FC, type FormEvent } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useQuotaTemplateMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { QuotaType, QuotaResetPeriod, type CreateQuotaInput } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport const QuotaTemplateCreatePage: 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 { createQuota, isCreating } = useQuotaTemplateMutations();\n\n // Form state\n const [name, setName] = useState('');\n const [displayName, setDisplayName] = useState('');\n const [description, setDescription] = useState('');\n const [limitType, setLimitType] = useState('numeric');\n const [limitValue, setLimitValue] = useState('');\n const [unit, setUnit] = useState('');\n const [quotaType, setQuotaType] = useState<QuotaType>(QuotaType.PRODUCT_SPECIFIC);\n const [reusable, setReusable] = useState(false);\n const [resetPeriod, setResetPeriod] = useState<QuotaResetPeriod>(QuotaResetPeriod.NEVER);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Permission check\n if (!permissions.canManageQuotas) {\n return <AccessDenied message=\"You don't have permission to create quotas.\" />;\n }\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n setSuccess(false);\n\n // Validation\n if (!name.trim()) {\n setError('Quota name is required');\n return;\n }\n if (!limitValue || isNaN(Number(limitValue)) || Number(limitValue) < 0) {\n setError('Valid limit value is required');\n return;\n }\n if (!unit.trim()) {\n setError('Unit is required (e.g., bots, GB, seats)');\n return;\n }\n\n const input: CreateQuotaInput = {\n name: name.trim(),\n limits: {\n type: limitType,\n value: Number(limitValue),\n unit: unit.trim(),\n },\n quotaType,\n reusable,\n resetPeriod,\n context: {\n displayName: displayName.trim() || name.trim(),\n description: description.trim(),\n },\n };\n\n try {\n await createQuota(input);\n setSuccess(true);\n // Navigate back after short delay\n setTimeout(() => navigateTo('/quotas'), 1500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create quota');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader title=\"Create New Quota\" description=\"Set up a new quota template\" />\n\n <PagePurpose>\n Define a reusable limit — its name, type (pooled or product-specific), unit and default\n value — that you can then attach to any number of plans and add-ons. Build the template once\n here instead of redefining the same limit on every plan.\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-6\">\n {/* Error/Success Messages */}\n {error && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n {success && (\n <div className=\"p-4 bg-status-success-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-success-text\">\n Quota created successfully! Redirecting…\n </p>\n </div>\n )}\n\n {/* Basic Info */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Basic Information</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Machine Key (name) */}\n <div>\n <label htmlFor=\"name\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Quota Key *\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] font-mono text-sm\"\n placeholder=\"e.g., sessions-started, tunnels-created\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Internal machine key, must match the <code>quotaName</code> used in{' '}\n <code>@usage</code> directives. Never shown to users.\n </p>\n </div>\n\n {/* Display Name */}\n <div>\n <label\n htmlFor=\"displayName\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Display Name *\n </label>\n <input\n id=\"displayName\"\n type=\"text\"\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"e.g., Tunnels, Session Starts, Webhooks\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Human-readable name shown to users in billing and usage UI.\n </p>\n </div>\n\n {/* Description */}\n <div>\n <label\n htmlFor=\"description\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Description\n </label>\n <textarea\n id=\"description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] resize-none\"\n placeholder=\"e.g., Active tunnels created through VibeControls (slot freed on delete)\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Short explanation shown in billing and usage detail views.\n </p>\n </div>\n\n {/* Quota Type */}\n <div>\n <label\n htmlFor=\"quotaType\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Quota Type *\n </label>\n <select\n id=\"quotaType\"\n value={quotaType}\n onChange={(e) => setQuotaType(e.target.value as QuotaType)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value={QuotaType.PRODUCT_SPECIFIC}>Product Specific</option>\n <option value={QuotaType.POOLED}>Pooled</option>\n </select>\n <p className=\"text-xs text-text-muted mt-1\">\n {quotaType === QuotaType.POOLED\n ? 'Shared across all products'\n : 'Specific to individual products'}\n </p>\n </div>\n\n {/* Reusable */}\n <div className=\"flex items-center gap-3\">\n <input\n id=\"reusable\"\n type=\"checkbox\"\n checked={reusable}\n onChange={(e) => setReusable(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <div>\n <label htmlFor=\"reusable\" className=\"text-sm font-medium text-text-primary\">\n Reusable Quota\n </label>\n <p className=\"text-xs text-text-muted\">\n Enable for quotas like seats or bots that can be freed up\n </p>\n </div>\n </div>\n\n {/* Reset Period */}\n <div>\n <label\n htmlFor=\"resetPeriod\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Reset Period\n </label>\n <select\n id=\"resetPeriod\"\n value={resetPeriod}\n onChange={(e) => setResetPeriod(e.target.value as QuotaResetPeriod)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value={QuotaResetPeriod.NEVER}>Never (one-time limit)</option>\n <option value={QuotaResetPeriod.DAILY}>Daily (resets every day)</option>\n <option value={QuotaResetPeriod.MONTHLY}>Monthly (resets every month)</option>\n </select>\n <p className=\"text-xs text-text-muted mt-1\">\n How often the quota usage counter is reset to zero\n </p>\n </div>\n </div>\n </div>\n\n {/* Limits Configuration */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Limits Configuration</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Limit Type */}\n <div>\n <label\n htmlFor=\"limitType\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Limit Type *\n </label>\n <select\n id=\"limitType\"\n value={limitType}\n onChange={(e) => setLimitType(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"numeric\">Numeric (count)</option>\n <option value=\"storage\">Storage (bytes)</option>\n <option value=\"duration\">Duration (seconds)</option>\n <option value=\"percentage\">Percentage</option>\n </select>\n </div>\n\n {/* Limit Value */}\n <div>\n <label\n htmlFor=\"limitValue\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Limit Value *\n </label>\n <input\n id=\"limitValue\"\n type=\"number\"\n min=\"0\"\n value={limitValue}\n onChange={(e) => setLimitValue(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"1000\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">The maximum limit value for this quota</p>\n </div>\n\n {/* Unit */}\n <div>\n <label htmlFor=\"unit\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Unit *\n </label>\n <input\n id=\"unit\"\n type=\"text\"\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"e.g., bots, GB, MB, seats, workflow runs\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n The unit of measurement displayed with usage values (e.g., "2/6 bots")\n </p>\n </div>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center gap-3 pt-4\">\n <button\n type=\"submit\"\n disabled={isCreating}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors disabled:opacity-50\"\n >\n {isCreating\n ? tr('billing.quota.creating', 'Creating...')\n : tr('billing.quota.createQuota', 'Create Quota')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas')}\n className=\"px-6 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Cancel\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuotaTemplateDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaTemplateDetailPage.tsx"],"sourcesContent":["/**\n * Quota Module - Quota Template Detail Page\n * Displays detailed information about a specific quota template\n */\n\nimport { type FC } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useQuotaTemplate, useQuotaTemplateMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader, ServerError } from '../../../shared/components';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { formatDate } from '../../../shared/utils/format';\nimport { isServerError } from '../../../shared/utils';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport const QuotaTemplateDetailPage: 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 { quotaId } = useParams<{ quotaId: string }>();\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { quota, isLoading, error, refetch } = useQuotaTemplate(quotaId);\n const { toggleQuota, isToggling } = useQuotaTemplateMutations();\n\n // Permission check\n if (!permissions.canViewQuotas) {\n return <AccessDenied message=\"You don't have permission to view quota details.\" />;\n }\n\n // Loading state\n if (isLoading) {\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 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-24 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n <div className=\"h-64 bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n );\n }\n\n // Error state\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title=\"Server Unavailable\"\n message=\"Unable to load quota details. The server might be down or experiencing issues.\"\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n // Not found state\n if (!quota) {\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-bg-sunken flex items-center justify-center\">\n <svg\n className=\"size-6 text-text-muted\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.quota.quotaNotFound', 'Quota not found')}\n </h2>\n <p className=\"text-sm text-text-muted\">\n The quota you're looking for doesn't exist or has been removed.\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Back to Quotas\n </button>\n </div>\n </div>\n );\n }\n\n const handleToggle = async () => {\n try {\n await toggleQuota(quota.id, !quota.isActive);\n refetch();\n } catch (err) {\n console.error('Failed to toggle quota:', err);\n }\n };\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 className=\"space-y-6 p-6\">\n <PageHeader\n title={quota.name}\n description={`${quota.quotaType === 'POOLED' ? 'Pooled' : 'Product Specific'} quota template`}\n />\n\n <PagePurpose>\n {tr(\n 'billing.quota.detailPurpose',\n 'Everything about this quota template — its limit configuration, type and where it is used. Review it here to understand what plans and add-ons referencing it will grant, before editing or attaching it elsewhere.'\n )}\n </PagePurpose>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Limits Card — primary emphasis zone */}\n <EmphasisPanel className=\"overflow-hidden p-0\">\n <div className=\"p-4 border-b border-border-subtle\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.quota.limitsConfig', 'Limits Configuration')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"grid grid-cols-3 gap-6\">\n <div>\n <p className=\"text-sm text-text-muted\">Limit Type</p>\n <p className=\"text-lg font-semibold text-text-primary capitalize mt-1\">\n {limitType}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">Limit Value</p>\n <p className=\"text-lg font-semibold text-text-primary mt-1\">\n {limitValue.toLocaleString()}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">Unit</p>\n <p className=\"text-lg font-semibold text-text-primary mt-1\">{limitUnit || '-'}</p>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n {/* Properties Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.quota.properties', 'Properties')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div className=\"flex items-center justify-between p-3 border border-border-subtle rounded-lg\">\n <div>\n <p className=\"font-medium text-text-primary\">Quota Type</p>\n <p className=\"text-sm text-text-muted\">\n {quota.quotaType === 'POOLED'\n ? 'Shared across all products'\n : 'Specific to individual products'}\n </p>\n </div>\n <span\n className={`px-3 py-1 text-sm 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\n <div className=\"flex items-center justify-between p-3 border border-border-subtle rounded-lg\">\n <div>\n <p className=\"font-medium text-text-primary\">Reusable</p>\n <p className=\"text-sm text-text-muted\">\n {quota.reusable\n ? 'Quota can be freed up and reused (e.g., seats, bots)'\n : 'Quota is consumed permanently (e.g., API calls, storage)'}\n </p>\n </div>\n <span\n className={`px-3 py-1 text-sm font-medium rounded ${\n quota.reusable\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {quota.reusable ? 'Yes' : 'No'}\n </span>\n </div>\n </div>\n </div>\n\n {/* Raw Limits JSON */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Limits (Raw JSON)</h2>\n </div>\n <div className=\"p-4\">\n <pre className=\"text-sm text-text-primary bg-bg-sunken/50 p-4 rounded-lg overflow-auto\">\n {JSON.stringify(quota.limits, null, 2)}\n </pre>\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Status Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Status</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-muted\">Status</span>\n <span\n className={`px-2 py-1 text-xs font-medium rounded ${\n quota.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {quota.isActive ? 'Active' : 'Inactive'}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-muted\">Created</span>\n <span className=\"text-sm font-medium text-text-primary\">\n {formatDate(quota.createdAt, 'short')}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-muted\">Updated</span>\n <span className=\"text-sm font-medium text-text-primary\">\n {formatDate(quota.updatedAt, 'short')}\n </span>\n </div>\n </div>\n </div>\n\n {/* Actions Card */}\n {permissions.canManageQuotas && (\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Actions</h2>\n </div>\n <div className=\"p-4 space-y-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo(`/quotas/${quota.id}/edit`)}\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 Edit Quota\n </button>\n <button\n type=\"button\"\n onClick={handleToggle}\n disabled={isToggling}\n className={`w-full px-4 py-2 text-sm font-medium border border-border-subtle rounded-md transition-colors disabled:opacity-50 ${\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 {isToggling\n ? 'Processing...'\n : quota.isActive\n ? 'Deactivate Quota'\n : 'Activate Quota'}\n </button>\n </div>\n </div>\n )}\n\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Back to Quotas\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"QuotaTemplateDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaTemplateDetailPage.tsx"],"sourcesContent":["/**\n * Quota Module - Quota Template Detail Page\n * Displays detailed information about a specific quota template\n */\n\nimport { type FC } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useQuotaTemplate, useQuotaTemplateMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader, ServerError } from '../../../shared/components';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { formatDate } from '../../../shared/utils/format';\nimport { isServerError } from '../../../shared/utils';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport const QuotaTemplateDetailPage: 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 { quotaId } = useParams<{ quotaId: string }>();\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { quota, isLoading, error, refetch } = useQuotaTemplate(quotaId);\n const { toggleQuota, isToggling } = useQuotaTemplateMutations();\n\n // Permission check\n if (!permissions.canViewQuotas) {\n return <AccessDenied message=\"You don't have permission to view quota details.\" />;\n }\n\n // Loading state\n if (isLoading) {\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 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-24 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n <div className=\"h-64 bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n );\n }\n\n // Error state\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title=\"Server Unavailable\"\n message=\"Unable to load quota details. The server might be down or experiencing issues.\"\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n // Not found state\n if (!quota) {\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-bg-sunken flex items-center justify-center\">\n <svg\n className=\"size-6 text-text-muted\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.quota.quotaNotFound', 'Quota not found')}\n </h2>\n <p className=\"text-sm text-text-muted\">\n The quota you're looking for doesn't exist or has been removed.\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Back to Quotas\n </button>\n </div>\n </div>\n );\n }\n\n const handleToggle = async () => {\n try {\n await toggleQuota(quota.id, !quota.isActive);\n refetch();\n } catch (err) {\n console.error('Failed to toggle quota:', err);\n }\n };\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 className=\"space-y-6 p-6\">\n <PageHeader\n title={quota.name}\n description={`${quota.quotaType === 'POOLED' ? 'Pooled' : 'Product Specific'} quota template`}\n />\n\n <PagePurpose>\n {tr(\n 'billing.quota.detailPurpose',\n 'Everything about this quota template — its limit configuration, type and where it is used. Review it here to understand what plans and add-ons referencing it will grant, before editing or attaching it elsewhere.'\n )}\n </PagePurpose>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Limits Card — primary emphasis zone */}\n <EmphasisPanel className=\"overflow-hidden p-0\">\n <div className=\"p-4 border-b border-border-subtle\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.quota.limitsConfig', 'Limits Configuration')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"grid grid-cols-3 gap-6\">\n <div>\n <p className=\"text-sm text-text-muted\">Limit Type</p>\n <p className=\"text-lg font-semibold text-text-primary capitalize mt-1\">\n {limitType}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">Limit Value</p>\n <p className=\"text-lg font-semibold text-text-primary mt-1\">\n {limitValue.toLocaleString()}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">Unit</p>\n <p className=\"text-lg font-semibold text-text-primary mt-1\">{limitUnit || '-'}</p>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n {/* Properties Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.quota.properties', 'Properties')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div className=\"flex items-center justify-between p-3 border border-border-subtle rounded-lg\">\n <div>\n <p className=\"font-medium text-text-primary\">Quota Type</p>\n <p className=\"text-sm text-text-muted\">\n {quota.quotaType === 'POOLED'\n ? 'Shared across all products'\n : 'Specific to individual products'}\n </p>\n </div>\n <span\n className={`px-3 py-1 text-sm 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\n <div className=\"flex items-center justify-between p-3 border border-border-subtle rounded-lg\">\n <div>\n <p className=\"font-medium text-text-primary\">Reusable</p>\n <p className=\"text-sm text-text-muted\">\n {quota.reusable\n ? 'Quota can be freed up and reused (e.g., seats, bots)'\n : 'Quota is consumed permanently (e.g., API calls, storage)'}\n </p>\n </div>\n <span\n className={`px-3 py-1 text-sm font-medium rounded ${\n quota.reusable\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {quota.reusable ? 'Yes' : 'No'}\n </span>\n </div>\n </div>\n </div>\n\n {/* Raw Limits JSON */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Limits (Raw JSON)</h2>\n </div>\n <div className=\"p-4\">\n <pre className=\"text-sm text-text-primary bg-bg-sunken/50 p-4 rounded-lg overflow-auto\">\n {JSON.stringify(quota.limits, null, 2)}\n </pre>\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Status Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Status</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-muted\">Status</span>\n <span\n className={`px-2 py-1 text-xs font-medium rounded ${\n quota.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {quota.isActive ? 'Active' : 'Inactive'}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-muted\">Created</span>\n <span className=\"text-sm font-medium text-text-primary\">\n {formatDate(quota.createdAt, 'short')}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-muted\">Updated</span>\n <span className=\"text-sm font-medium text-text-primary\">\n {formatDate(quota.updatedAt, 'short')}\n </span>\n </div>\n </div>\n </div>\n\n {/* Actions Card */}\n {permissions.canManageQuotas && (\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Actions</h2>\n </div>\n <div className=\"p-4 space-y-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo(`/quotas/${quota.id}/edit`)}\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 Edit Quota\n </button>\n <button\n type=\"button\"\n onClick={handleToggle}\n disabled={isToggling}\n className={`w-full px-4 py-2 text-sm font-medium border border-border-subtle rounded-md transition-colors disabled:opacity-50 ${\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 {isToggling\n ? 'Processing...'\n : quota.isActive\n ? 'Deactivate Quota'\n : 'Activate Quota'}\n </button>\n </div>\n </div>\n )}\n\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Back to Quotas\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuotaTemplateEditPage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaTemplateEditPage.tsx"],"sourcesContent":["/**\n * Quota Module - Quota Template Edit Page\n * Form to edit an existing quota template\n */\n\nimport { useState, useEffect, type FC, type FormEvent } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useQuotaTemplate, useQuotaTemplateMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader, ServerError } from '../../../shared/components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { isServerError } from '../../../shared/utils';\nimport { QuotaResetPeriod, type UpdateQuotaInput } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport const QuotaTemplateEditPage: 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 { quotaId } = useParams<{ quotaId: string }>();\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const {\n quota,\n isLoading: isQuotaLoading,\n error: quotaError,\n refetch,\n } = useQuotaTemplate(quotaId);\n const { updateQuota, isUpdating } = useQuotaTemplateMutations();\n\n // Form state\n const [name, setName] = useState('');\n const [displayName, setDisplayName] = useState('');\n const [description, setDescription] = useState('');\n const [limitType, setLimitType] = useState('numeric');\n const [limitValue, setLimitValue] = useState('');\n const [unit, setUnit] = useState('');\n const [reusable, setReusable] = useState(false);\n const [resetPeriod, setResetPeriod] = useState<QuotaResetPeriod>(QuotaResetPeriod.NEVER);\n const [isActive, setIsActive] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Populate form when quota data loads\n useEffect(() => {\n if (quota) {\n setName(quota.name ?? '');\n setReusable(quota.reusable ?? false);\n setResetPeriod(quota.resetPeriod ?? QuotaResetPeriod.NEVER);\n setIsActive(quota.isActive ?? true);\n\n const limits = quota.limits as { type?: string; value?: number; unit?: string } | null;\n setLimitType(limits?.type ?? 'numeric');\n setLimitValue(String(limits?.value ?? ''));\n setUnit(limits?.unit ?? '');\n\n const ctx = quota.context as { displayName?: string; description?: string } | null;\n setDisplayName(ctx?.displayName ?? '');\n setDescription(ctx?.description ?? '');\n }\n }, [quota]);\n\n // Permission check\n if (!permissions.canManageQuotas) {\n return <AccessDenied message=\"You don't have permission to edit quotas.\" />;\n }\n\n // Loading state\n if (isQuotaLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"max-w-2xl space-y-4\">\n {[1, 2, 3, 4].map((i) => (\n <div key={i} className=\"h-16 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (quotaError && isServerError(quotaError)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title=\"Server Unavailable\"\n message=\"Unable to load quota. The server might be down or experiencing issues.\"\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!quota) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Quota not found</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas')}\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\"\n >\n Back to Quotas\n </button>\n </div>\n </div>\n );\n }\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n setSuccess(false);\n\n // Validation\n if (!name.trim()) {\n setError('Quota name is required');\n return;\n }\n if (!limitValue || isNaN(Number(limitValue)) || Number(limitValue) < 0) {\n setError('Valid limit value is required');\n return;\n }\n if (!unit.trim()) {\n setError('Unit is required (e.g., bots, GB, seats)');\n return;\n }\n\n const input: UpdateQuotaInput = {\n id: quotaId!,\n name: name.trim(),\n limits: {\n type: limitType,\n value: Number(limitValue),\n unit: unit.trim(),\n },\n reusable,\n isActive,\n resetPeriod,\n context: {\n displayName: displayName.trim() || name.trim(),\n description: description.trim(),\n },\n };\n\n try {\n await updateQuota(input);\n setSuccess(true);\n // Navigate back after short delay\n setTimeout(() => navigateTo(`/quotas/${quotaId}`), 1500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update quota');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={`Edit Quota: ${(quota.context as { displayName?: string } | null)?.displayName ?? quota.name}`}\n description=\"Update the quota template configuration\"\n />\n\n <PagePurpose>\n Adjust this quota template's configuration. Changes apply wherever the template is\n referenced, so review which plans and add-ons use it before changing its type or unit.\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-6\">\n {/* Error/Success Messages */}\n {error && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n {success && (\n <div className=\"p-4 bg-status-success-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-success-text\">\n Quota updated successfully! Redirecting…\n </p>\n </div>\n )}\n\n {/* Basic Info */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Basic Information</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Machine Key (name) */}\n <div>\n <label htmlFor=\"name\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Quota Key *\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] font-mono text-sm\"\n placeholder=\"e.g., sessions-started, tunnels-created\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Internal machine key, must match the <code>quotaName</code> used in{' '}\n <code>@usage</code> directives. Never shown to users.\n </p>\n </div>\n\n {/* Display Name */}\n <div>\n <label\n htmlFor=\"displayName\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Display Name *\n </label>\n <input\n id=\"displayName\"\n type=\"text\"\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"e.g., Tunnels, Session Starts, Webhooks\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Human-readable name shown to users in billing and usage UI.\n </p>\n </div>\n\n {/* Description */}\n <div>\n <label\n htmlFor=\"description\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Description\n </label>\n <textarea\n id=\"description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] resize-none\"\n placeholder=\"e.g., Active tunnels created through VibeControls (slot freed on delete)\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Short explanation shown in billing and usage detail views.\n </p>\n </div>\n\n {/* Quota Type (read-only) */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">Quota Type</label>\n <div className=\"px-3 py-2 border border-border-subtle rounded-md bg-bg-sunken/50 text-text-muted\">\n {quota.quotaType === 'POOLED' ? 'Pooled' : 'Product Specific'}\n <span className=\"text-xs ml-2\">(cannot be changed)</span>\n </div>\n </div>\n\n {/* Reusable */}\n <div className=\"flex items-center gap-3\">\n <input\n id=\"reusable\"\n type=\"checkbox\"\n checked={reusable}\n onChange={(e) => setReusable(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <div>\n <label htmlFor=\"reusable\" className=\"text-sm font-medium text-text-primary\">\n Reusable Quota\n </label>\n <p className=\"text-xs text-text-muted\">\n Enable for quotas like seats or bots that can be freed up\n </p>\n </div>\n </div>\n\n {/* Reset Period */}\n <div>\n <label\n htmlFor=\"resetPeriod\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Reset Period\n </label>\n <select\n id=\"resetPeriod\"\n value={resetPeriod}\n onChange={(e) => setResetPeriod(e.target.value as QuotaResetPeriod)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value={QuotaResetPeriod.NEVER}>Never (one-time limit)</option>\n <option value={QuotaResetPeriod.DAILY}>Daily (resets every day)</option>\n <option value={QuotaResetPeriod.MONTHLY}>Monthly (resets every month)</option>\n </select>\n <p className=\"text-xs text-text-muted mt-1\">\n How often the quota usage counter is reset to zero\n </p>\n </div>\n\n {/* Status */}\n <div className=\"flex items-center gap-3\">\n <input\n id=\"isActive\"\n type=\"checkbox\"\n checked={isActive}\n onChange={(e) => setIsActive(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <label htmlFor=\"isActive\" className=\"text-sm font-medium text-text-primary\">\n Quota is active\n </label>\n </div>\n </div>\n </div>\n\n {/* Limits Configuration */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Limits Configuration</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Limit Type */}\n <div>\n <label\n htmlFor=\"limitType\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Limit Type *\n </label>\n <select\n id=\"limitType\"\n value={limitType}\n onChange={(e) => setLimitType(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"numeric\">Numeric (count)</option>\n <option value=\"storage\">Storage (bytes)</option>\n <option value=\"duration\">Duration (seconds)</option>\n <option value=\"percentage\">Percentage</option>\n </select>\n </div>\n\n {/* Limit Value */}\n <div>\n <label\n htmlFor=\"limitValue\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Limit Value *\n </label>\n <input\n id=\"limitValue\"\n type=\"number\"\n min=\"0\"\n value={limitValue}\n onChange={(e) => setLimitValue(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"1000\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">The maximum limit value for this quota</p>\n </div>\n\n {/* Unit */}\n <div>\n <label htmlFor=\"unit\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Unit *\n </label>\n <input\n id=\"unit\"\n type=\"text\"\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"e.g., bots, GB, MB, seats, workflow runs\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n The unit of measurement displayed with usage values (e.g., "2/6 bots")\n </p>\n </div>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center gap-3 pt-4\">\n <button\n type=\"submit\"\n disabled={isUpdating}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors disabled:opacity-50\"\n >\n {isUpdating\n ? tr('billing.quota.saving', 'Saving...')\n : tr('billing.quota.saveChanges', 'Save Changes')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/quotas/${quotaId}`)}\n className=\"px-6 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Cancel\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"QuotaTemplateEditPage.js","names":[],"sources":["../../../../../src/billing/modules/quota/pages/QuotaTemplateEditPage.tsx"],"sourcesContent":["/**\n * Quota Module - Quota Template Edit Page\n * Form to edit an existing quota template\n */\n\nimport { useState, useEffect, type FC, type FormEvent } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useQuotaTemplate, useQuotaTemplateMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader, ServerError } from '../../../shared/components';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { isServerError } from '../../../shared/utils';\nimport { QuotaResetPeriod, type UpdateQuotaInput } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport const QuotaTemplateEditPage: 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 { quotaId } = useParams<{ quotaId: string }>();\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const {\n quota,\n isLoading: isQuotaLoading,\n error: quotaError,\n refetch,\n } = useQuotaTemplate(quotaId);\n const { updateQuota, isUpdating } = useQuotaTemplateMutations();\n\n // Form state\n const [name, setName] = useState('');\n const [displayName, setDisplayName] = useState('');\n const [description, setDescription] = useState('');\n const [limitType, setLimitType] = useState('numeric');\n const [limitValue, setLimitValue] = useState('');\n const [unit, setUnit] = useState('');\n const [reusable, setReusable] = useState(false);\n const [resetPeriod, setResetPeriod] = useState<QuotaResetPeriod>(QuotaResetPeriod.NEVER);\n const [isActive, setIsActive] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Populate form when quota data loads\n useEffect(() => {\n if (quota) {\n setName(quota.name ?? '');\n setReusable(quota.reusable ?? false);\n setResetPeriod(quota.resetPeriod ?? QuotaResetPeriod.NEVER);\n setIsActive(quota.isActive ?? true);\n\n const limits = quota.limits as { type?: string; value?: number; unit?: string } | null;\n setLimitType(limits?.type ?? 'numeric');\n setLimitValue(String(limits?.value ?? ''));\n setUnit(limits?.unit ?? '');\n\n const ctx = quota.context as { displayName?: string; description?: string } | null;\n setDisplayName(ctx?.displayName ?? '');\n setDescription(ctx?.description ?? '');\n }\n }, [quota]);\n\n // Permission check\n if (!permissions.canManageQuotas) {\n return <AccessDenied message=\"You don't have permission to edit quotas.\" />;\n }\n\n // Loading state\n if (isQuotaLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"max-w-2xl space-y-4\">\n {[1, 2, 3, 4].map((i) => (\n <div key={i} className=\"h-16 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (quotaError && isServerError(quotaError)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title=\"Server Unavailable\"\n message=\"Unable to load quota. The server might be down or experiencing issues.\"\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!quota) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Quota not found</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/quotas')}\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\"\n >\n Back to Quotas\n </button>\n </div>\n </div>\n );\n }\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n setSuccess(false);\n\n // Validation\n if (!name.trim()) {\n setError('Quota name is required');\n return;\n }\n if (!limitValue || isNaN(Number(limitValue)) || Number(limitValue) < 0) {\n setError('Valid limit value is required');\n return;\n }\n if (!unit.trim()) {\n setError('Unit is required (e.g., bots, GB, seats)');\n return;\n }\n\n const input: UpdateQuotaInput = {\n id: quotaId!,\n name: name.trim(),\n limits: {\n type: limitType,\n value: Number(limitValue),\n unit: unit.trim(),\n },\n reusable,\n isActive,\n resetPeriod,\n context: {\n displayName: displayName.trim() || name.trim(),\n description: description.trim(),\n },\n };\n\n try {\n await updateQuota(input);\n setSuccess(true);\n // Navigate back after short delay\n setTimeout(() => navigateTo(`/quotas/${quotaId}`), 1500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update quota');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={`Edit Quota: ${(quota.context as { displayName?: string } | null)?.displayName ?? quota.name}`}\n description=\"Update the quota template configuration\"\n />\n\n <PagePurpose>\n Adjust this quota template's configuration. Changes apply wherever the template is\n referenced, so review which plans and add-ons use it before changing its type or unit.\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-6\">\n {/* Error/Success Messages */}\n {error && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n {success && (\n <div className=\"p-4 bg-status-success-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-success-text\">\n Quota updated successfully! Redirecting…\n </p>\n </div>\n )}\n\n {/* Basic Info */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Basic Information</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Machine Key (name) */}\n <div>\n <label htmlFor=\"name\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Quota Key *\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] font-mono text-sm\"\n placeholder=\"e.g., sessions-started, tunnels-created\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Internal machine key, must match the <code>quotaName</code> used in{' '}\n <code>@usage</code> directives. Never shown to users.\n </p>\n </div>\n\n {/* Display Name */}\n <div>\n <label\n htmlFor=\"displayName\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Display Name *\n </label>\n <input\n id=\"displayName\"\n type=\"text\"\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"e.g., Tunnels, Session Starts, Webhooks\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Human-readable name shown to users in billing and usage UI.\n </p>\n </div>\n\n {/* Description */}\n <div>\n <label\n htmlFor=\"description\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Description\n </label>\n <textarea\n id=\"description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] resize-none\"\n placeholder=\"e.g., Active tunnels created through VibeControls (slot freed on delete)\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Short explanation shown in billing and usage detail views.\n </p>\n </div>\n\n {/* Quota Type (read-only) */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">Quota Type</label>\n <div className=\"px-3 py-2 border border-border-subtle rounded-md bg-bg-sunken/50 text-text-muted\">\n {quota.quotaType === 'POOLED' ? 'Pooled' : 'Product Specific'}\n <span className=\"text-xs ml-2\">(cannot be changed)</span>\n </div>\n </div>\n\n {/* Reusable */}\n <div className=\"flex items-center gap-3\">\n <input\n id=\"reusable\"\n type=\"checkbox\"\n checked={reusable}\n onChange={(e) => setReusable(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <div>\n <label htmlFor=\"reusable\" className=\"text-sm font-medium text-text-primary\">\n Reusable Quota\n </label>\n <p className=\"text-xs text-text-muted\">\n Enable for quotas like seats or bots that can be freed up\n </p>\n </div>\n </div>\n\n {/* Reset Period */}\n <div>\n <label\n htmlFor=\"resetPeriod\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Reset Period\n </label>\n <select\n id=\"resetPeriod\"\n value={resetPeriod}\n onChange={(e) => setResetPeriod(e.target.value as QuotaResetPeriod)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value={QuotaResetPeriod.NEVER}>Never (one-time limit)</option>\n <option value={QuotaResetPeriod.DAILY}>Daily (resets every day)</option>\n <option value={QuotaResetPeriod.MONTHLY}>Monthly (resets every month)</option>\n </select>\n <p className=\"text-xs text-text-muted mt-1\">\n How often the quota usage counter is reset to zero\n </p>\n </div>\n\n {/* Status */}\n <div className=\"flex items-center gap-3\">\n <input\n id=\"isActive\"\n type=\"checkbox\"\n checked={isActive}\n onChange={(e) => setIsActive(e.target.checked)}\n className=\"size-4 rounded border-border-subtle text-text-link focus:ring-[var(--color-focus-ring)]\"\n />\n <label htmlFor=\"isActive\" className=\"text-sm font-medium text-text-primary\">\n Quota is active\n </label>\n </div>\n </div>\n </div>\n\n {/* Limits Configuration */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Limits Configuration</h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Limit Type */}\n <div>\n <label\n htmlFor=\"limitType\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Limit Type *\n </label>\n <select\n id=\"limitType\"\n value={limitType}\n onChange={(e) => setLimitType(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"numeric\">Numeric (count)</option>\n <option value=\"storage\">Storage (bytes)</option>\n <option value=\"duration\">Duration (seconds)</option>\n <option value=\"percentage\">Percentage</option>\n </select>\n </div>\n\n {/* Limit Value */}\n <div>\n <label\n htmlFor=\"limitValue\"\n className=\"block text-sm font-medium text-text-primary mb-1\"\n >\n Limit Value *\n </label>\n <input\n id=\"limitValue\"\n type=\"number\"\n min=\"0\"\n value={limitValue}\n onChange={(e) => setLimitValue(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"1000\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">The maximum limit value for this quota</p>\n </div>\n\n {/* Unit */}\n <div>\n <label htmlFor=\"unit\" className=\"block text-sm font-medium text-text-primary mb-1\">\n Unit *\n </label>\n <input\n id=\"unit\"\n type=\"text\"\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"e.g., bots, GB, MB, seats, workflow runs\"\n required\n />\n <p className=\"text-xs text-text-muted mt-1\">\n The unit of measurement displayed with usage values (e.g., "2/6 bots")\n </p>\n </div>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center gap-3 pt-4\">\n <button\n type=\"submit\"\n disabled={isUpdating}\n className=\"px-6 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors disabled:opacity-50\"\n >\n {isUpdating\n ? tr('billing.quota.saving', 'Saving...')\n : tr('billing.quota.saveChanges', 'Save Changes')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/quotas/${quotaId}`)}\n className=\"px-6 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Cancel\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|